codepiper 0.1.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 (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
@@ -0,0 +1,969 @@
1
+ /**
2
+ * WorkflowRunner - executes workflow definitions
3
+ */
4
+
5
+ import type { EventBus } from "@codepiper/core";
6
+ import type { Database } from "../db/db";
7
+ import { createWorkflowDb } from "../db/workflowDb";
8
+ import type { SessionManager } from "../sessions/sessionManager";
9
+ import { ContextManager } from "./contextManager";
10
+ import { ResultExtractor } from "./resultExtractor";
11
+ import { WaitConditionPoller } from "./waitConditionPoller";
12
+ import type {
13
+ ConditionalStep,
14
+ LogStep,
15
+ LoopStep,
16
+ ParallelStep,
17
+ SessionStep,
18
+ StepResult,
19
+ WorkflowDefinition,
20
+ WorkflowExecution,
21
+ WorkflowStep,
22
+ } from "./workflowTypes";
23
+
24
+ export interface WorkflowRunnerOptions {
25
+ sessionManager: SessionManager;
26
+ database: Database;
27
+ eventBus: EventBus;
28
+ }
29
+
30
+ export class WorkflowRunner {
31
+ private sessionManager: SessionManager;
32
+ private database: Database;
33
+ private eventBus: EventBus;
34
+ private executions: Map<string, WorkflowExecution>;
35
+ private poller: WaitConditionPoller;
36
+ private extractor: ResultExtractor;
37
+
38
+ constructor(options: WorkflowRunnerOptions) {
39
+ this.sessionManager = options.sessionManager;
40
+ this.database = options.database;
41
+ this.eventBus = options.eventBus;
42
+ this.executions = new Map();
43
+ this.poller = new WaitConditionPoller(this.database);
44
+ this.extractor = new ResultExtractor(this.database);
45
+ }
46
+
47
+ /**
48
+ * Start workflow execution
49
+ * @param workflow - Workflow definition
50
+ * @param executionId - Optional execution ID (if not provided, generates new one)
51
+ * @param variables - Initial variables for context
52
+ * @returns Execution ID
53
+ */
54
+ async start(
55
+ workflow: WorkflowDefinition,
56
+ executionId?: string,
57
+ variables: Record<string, any> = {}
58
+ ): Promise<string> {
59
+ const execId = executionId || crypto.randomUUID();
60
+ const isNewExecution = !executionId;
61
+
62
+ const execution: WorkflowExecution = {
63
+ id: execId,
64
+ workflowId: workflow.name,
65
+ status: "running",
66
+ startedAt: new Date(),
67
+ context: { ...variables },
68
+ stepResults: new Map(),
69
+ };
70
+
71
+ this.executions.set(execId, execution);
72
+
73
+ // Create execution in database if this is a new execution (not passed from API)
74
+ if (isNewExecution) {
75
+ const workflowDb = createWorkflowDb(this.database);
76
+ workflowDb.createExecution({
77
+ id: execId,
78
+ workflowId: workflow.name,
79
+ status: "running",
80
+ context: variables,
81
+ });
82
+ }
83
+
84
+ // Execute workflow asynchronously
85
+ this.executeWorkflow(execId, workflow).catch((error) => {
86
+ const exec = this.executions.get(execId);
87
+ if (exec && exec.status === "running") {
88
+ exec.status = "failed";
89
+ exec.error = error instanceof Error ? error.message : String(error);
90
+ exec.completedAt = new Date();
91
+
92
+ // Update database
93
+ const workflowDb = createWorkflowDb(this.database);
94
+ workflowDb.updateExecution(execId, {
95
+ status: "failed",
96
+ errorMessage: exec.error,
97
+ completedAt: exec.completedAt,
98
+ });
99
+ }
100
+ });
101
+
102
+ return execId;
103
+ }
104
+
105
+ /**
106
+ * Get execution by ID
107
+ */
108
+ getExecution(executionId: string): WorkflowExecution | undefined {
109
+ return this.executions.get(executionId);
110
+ }
111
+
112
+ /**
113
+ * List all executions
114
+ */
115
+ listExecutions(): WorkflowExecution[] {
116
+ return Array.from(this.executions.values());
117
+ }
118
+
119
+ /**
120
+ * Cancel running execution
121
+ */
122
+ async cancel(executionId: string): Promise<void> {
123
+ const execution = this.executions.get(executionId);
124
+ if (!execution) {
125
+ throw new Error(`Execution ${executionId} not found`);
126
+ }
127
+
128
+ if (execution.status !== "running") {
129
+ throw new Error(`Execution ${executionId} is not running`);
130
+ }
131
+
132
+ execution.status = "cancelled";
133
+ execution.completedAt = new Date();
134
+
135
+ // Update database
136
+ const workflowDb = createWorkflowDb(this.database);
137
+ workflowDb.updateExecution(executionId, {
138
+ status: "cancelled",
139
+ completedAt: execution.completedAt,
140
+ });
141
+
142
+ // Stop any running sessions
143
+ for (const [_stepName, stepResult] of execution.stepResults) {
144
+ if (stepResult.sessionId && stepResult.status === "running") {
145
+ try {
146
+ await this.sessionManager.stopSession(stepResult.sessionId);
147
+ } catch {
148
+ // Ignore errors during cleanup
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Wait for execution to complete
156
+ * @param executionId - Execution ID
157
+ * @param timeout - Max wait time in ms
158
+ */
159
+ async waitForCompletion(executionId: string, timeout: number): Promise<void> {
160
+ const startTime = Date.now();
161
+
162
+ while (true) {
163
+ const execution = this.executions.get(executionId);
164
+ if (!execution) {
165
+ throw new Error(`Execution ${executionId} not found`);
166
+ }
167
+
168
+ if (execution.status !== "running") {
169
+ return;
170
+ }
171
+
172
+ if (Date.now() - startTime > timeout) {
173
+ throw new Error(`Workflow execution timeout after ${timeout}ms`);
174
+ }
175
+
176
+ await new Promise((resolve) => setTimeout(resolve, 100));
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Execute workflow steps
182
+ */
183
+ private async executeWorkflow(executionId: string, workflow: WorkflowDefinition): Promise<void> {
184
+ const execution = this.executions.get(executionId);
185
+ if (!execution) {
186
+ throw new Error(`Execution ${executionId} not found`);
187
+ }
188
+
189
+ const workflowDb = createWorkflowDb(this.database);
190
+ const contextManager = new ContextManager();
191
+
192
+ // Initialize context with variables
193
+ for (const [key, value] of Object.entries(execution.context)) {
194
+ contextManager.set(key, value);
195
+ }
196
+
197
+ // Update execution status to running in database
198
+ workflowDb.updateExecution(executionId, {
199
+ status: "running",
200
+ });
201
+
202
+ try {
203
+ // Execute steps sequentially
204
+ for (const step of workflow.steps) {
205
+ // Check if execution was cancelled
206
+ if (execution.status === "cancelled") {
207
+ return;
208
+ }
209
+
210
+ await this.executeStepWithErrorHandling(executionId, step, contextManager);
211
+ }
212
+
213
+ // Workflow completed successfully
214
+ execution.status = "completed";
215
+ execution.completedAt = new Date();
216
+
217
+ // Update database
218
+ workflowDb.updateExecution(executionId, {
219
+ status: "completed",
220
+ completedAt: execution.completedAt,
221
+ });
222
+ } catch (error) {
223
+ if (execution.status !== "cancelled") {
224
+ execution.status = "failed";
225
+ execution.error = error instanceof Error ? error.message : String(error);
226
+ execution.completedAt = new Date();
227
+
228
+ // Update database
229
+ workflowDb.updateExecution(executionId, {
230
+ status: "failed",
231
+ errorMessage: execution.error,
232
+ completedAt: execution.completedAt,
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Execute a workflow step and apply session error strategy when configured.
240
+ */
241
+ private async executeStepWithErrorHandling(
242
+ executionId: string,
243
+ step: WorkflowStep,
244
+ contextManager: ContextManager
245
+ ): Promise<StepResult> {
246
+ if (step.type !== "session") {
247
+ const result = await this.executeStep(executionId, step, contextManager);
248
+ if (result.status === "failed") {
249
+ throw new Error(`Step ${step.name} failed: ${result.error}`);
250
+ }
251
+ return result;
252
+ }
253
+
254
+ const maxAttempts = step.onError === "retry" ? Math.max(1, step.retry?.maxAttempts ?? 1) : 1;
255
+ const retryDelayMs = Math.max(0, step.retry?.delay ?? 0);
256
+
257
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
258
+ const execution = this.executions.get(executionId);
259
+ if (!execution || execution.status === "cancelled") {
260
+ return {
261
+ status: "skipped",
262
+ startedAt: new Date(),
263
+ completedAt: new Date(),
264
+ };
265
+ }
266
+
267
+ const stepResult = await this.executeSessionStep(executionId, step, contextManager);
268
+ if (stepResult.status !== "failed") {
269
+ return stepResult;
270
+ }
271
+
272
+ if (step.onError === "continue") {
273
+ return stepResult;
274
+ }
275
+
276
+ if (step.onError === "retry" && attempt < maxAttempts) {
277
+ if (retryDelayMs > 0) {
278
+ await this.sleep(retryDelayMs);
279
+ }
280
+ continue;
281
+ }
282
+
283
+ if (step.onError === "retry") {
284
+ throw new Error(
285
+ `Step ${step.name} failed after ${maxAttempts} attempt(s): ${stepResult.error}`
286
+ );
287
+ }
288
+
289
+ throw new Error(`Step ${step.name} failed: ${stepResult.error}`);
290
+ }
291
+
292
+ throw new Error(`Step ${step.name} failed unexpectedly`);
293
+ }
294
+
295
+ /**
296
+ * Execute a workflow step by type.
297
+ */
298
+ private async executeStep(
299
+ executionId: string,
300
+ step: WorkflowStep,
301
+ contextManager: ContextManager
302
+ ): Promise<StepResult> {
303
+ switch (step.type) {
304
+ case "session":
305
+ return this.executeSessionStep(executionId, step, contextManager);
306
+ case "if":
307
+ return this.executeConditionalStep(executionId, step, contextManager);
308
+ case "parallel":
309
+ return this.executeParallelStep(executionId, step, contextManager);
310
+ case "foreach":
311
+ return this.executeLoopStep(executionId, step, contextManager);
312
+ case "log":
313
+ return this.executeLogStep(executionId, step, contextManager);
314
+ default:
315
+ return {
316
+ status: "failed",
317
+ error: `Unsupported step type: ${(step as { type: string }).type}`,
318
+ startedAt: new Date(),
319
+ completedAt: new Date(),
320
+ };
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Execute a session step.
326
+ */
327
+ private async executeSessionStep(
328
+ executionId: string,
329
+ step: SessionStep,
330
+ contextManager: ContextManager
331
+ ): Promise<StepResult> {
332
+ const execution = this.executions.get(executionId);
333
+ if (!execution) {
334
+ throw new Error(`Execution ${executionId} not found`);
335
+ }
336
+
337
+ const workflowDb = createWorkflowDb(this.database);
338
+
339
+ // Initialize step result
340
+ const stepResult: StepResult = {
341
+ status: "running",
342
+ startedAt: new Date(),
343
+ };
344
+ execution.stepResults.set(step.name, stepResult);
345
+
346
+ // Create step record in database
347
+ const stepId = workflowDb.createStep({
348
+ executionId,
349
+ stepName: step.name,
350
+ status: "running",
351
+ });
352
+
353
+ try {
354
+ // Substitute variables in cwd and prompt
355
+ const cwd = contextManager.substitute(step.cwd);
356
+ const prompt = step.prompt ? contextManager.substitute(step.prompt) : undefined;
357
+
358
+ // Create session
359
+ const session = await this.sessionManager.createSession({
360
+ provider: step.provider,
361
+ cwd,
362
+ env: step.env,
363
+ args: step.args,
364
+ });
365
+
366
+ stepResult.sessionId = session.id;
367
+
368
+ // Update step with sessionId
369
+ workflowDb.updateStep(stepId, {
370
+ sessionId: session.id,
371
+ startedAt: stepResult.startedAt,
372
+ });
373
+
374
+ // Send prompt if provided
375
+ if (prompt) {
376
+ await this.sessionManager.sendText(session.id, prompt);
377
+ await this.sessionManager.sendKeys(session.id, ["enter"]);
378
+ }
379
+
380
+ // Wait for conditions if provided
381
+ if (step.wait && step.wait.length > 0) {
382
+ await this.poller.wait(session.id, step.wait);
383
+ }
384
+
385
+ // Extract results if configured
386
+ if (step.extract) {
387
+ const extractedData: Record<string, any> = {};
388
+
389
+ for (const [key, config] of Object.entries(step.extract)) {
390
+ const value = await this.extractor.extract(session.id, config);
391
+ extractedData[key] = value;
392
+
393
+ this.setStepContextValue(contextManager, execution, step.name, key, value);
394
+ }
395
+
396
+ stepResult.extractedData = extractedData;
397
+ }
398
+
399
+ // Mark step as completed
400
+ stepResult.status = "completed";
401
+ stepResult.completedAt = new Date();
402
+
403
+ // Update step in database
404
+ workflowDb.updateStep(stepId, {
405
+ status: "completed",
406
+ completedAt: stepResult.completedAt,
407
+ result: stepResult.extractedData ? JSON.stringify(stepResult.extractedData) : undefined,
408
+ });
409
+ return stepResult;
410
+ } catch (error) {
411
+ stepResult.status = "failed";
412
+ stepResult.error = error instanceof Error ? error.message : String(error);
413
+ stepResult.completedAt = new Date();
414
+
415
+ // Update step in database
416
+ workflowDb.updateStep(stepId, {
417
+ status: "failed",
418
+ errorMessage: stepResult.error,
419
+ completedAt: stepResult.completedAt,
420
+ });
421
+ return stepResult;
422
+ }
423
+ }
424
+
425
+ private async executeConditionalStep(
426
+ executionId: string,
427
+ step: ConditionalStep,
428
+ contextManager: ContextManager
429
+ ): Promise<StepResult> {
430
+ const execution = this.executions.get(executionId);
431
+ if (!execution) {
432
+ throw new Error(`Execution ${executionId} not found`);
433
+ }
434
+
435
+ const workflowDb = createWorkflowDb(this.database);
436
+ const stepResult: StepResult = {
437
+ status: "running",
438
+ startedAt: new Date(),
439
+ };
440
+ execution.stepResults.set(step.name, stepResult);
441
+
442
+ const stepId = workflowDb.createStep({
443
+ executionId,
444
+ stepName: step.name,
445
+ status: "running",
446
+ });
447
+
448
+ try {
449
+ const conditionMatched = this.evaluateCondition(step.condition, contextManager);
450
+ const selectedBranch = conditionMatched ? step.then : (step.else ?? []);
451
+ const branchName = conditionMatched ? "then" : "else";
452
+
453
+ for (const childStep of selectedBranch) {
454
+ await this.executeStepWithErrorHandling(executionId, childStep, contextManager);
455
+ }
456
+
457
+ stepResult.status = "completed";
458
+ stepResult.completedAt = new Date();
459
+ stepResult.extractedData = {
460
+ branch: branchName,
461
+ matched: conditionMatched,
462
+ };
463
+
464
+ workflowDb.updateStep(stepId, {
465
+ status: "completed",
466
+ completedAt: stepResult.completedAt,
467
+ result: JSON.stringify(stepResult.extractedData),
468
+ });
469
+
470
+ return stepResult;
471
+ } catch (error) {
472
+ stepResult.status = "failed";
473
+ stepResult.error = error instanceof Error ? error.message : String(error);
474
+ stepResult.completedAt = new Date();
475
+
476
+ workflowDb.updateStep(stepId, {
477
+ status: "failed",
478
+ errorMessage: stepResult.error,
479
+ completedAt: stepResult.completedAt,
480
+ });
481
+
482
+ return stepResult;
483
+ }
484
+ }
485
+
486
+ private async executeParallelStep(
487
+ executionId: string,
488
+ step: ParallelStep,
489
+ contextManager: ContextManager
490
+ ): Promise<StepResult> {
491
+ const execution = this.executions.get(executionId);
492
+ if (!execution) {
493
+ throw new Error(`Execution ${executionId} not found`);
494
+ }
495
+
496
+ const workflowDb = createWorkflowDb(this.database);
497
+ const stepResult: StepResult = {
498
+ status: "running",
499
+ startedAt: new Date(),
500
+ };
501
+ execution.stepResults.set(step.name, stepResult);
502
+
503
+ const stepId = workflowDb.createStep({
504
+ executionId,
505
+ stepName: step.name,
506
+ status: "running",
507
+ });
508
+
509
+ try {
510
+ const waitFor = step.waitFor ?? "all";
511
+ const branchPromises = step.steps.map((childStep) =>
512
+ this.executeParallelBranch(executionId, childStep, contextManager)
513
+ );
514
+
515
+ if (waitFor === "none") {
516
+ // Fire-and-forget branch execution: workflow continues immediately.
517
+ for (const promise of branchPromises) {
518
+ void promise.catch(() => {
519
+ // Branch errors are intentionally decoupled from parent step completion.
520
+ });
521
+ }
522
+
523
+ stepResult.status = "completed";
524
+ stepResult.completedAt = new Date();
525
+ stepResult.extractedData = {
526
+ waitFor,
527
+ launched: step.steps.length,
528
+ };
529
+
530
+ workflowDb.updateStep(stepId, {
531
+ status: "completed",
532
+ completedAt: stepResult.completedAt,
533
+ result: JSON.stringify(stepResult.extractedData),
534
+ });
535
+
536
+ return stepResult;
537
+ }
538
+
539
+ if (waitFor === "any") {
540
+ try {
541
+ const winner = await Promise.any(
542
+ branchPromises.map(async (promise) => {
543
+ const result = await promise;
544
+ if (result.status === "completed") {
545
+ return result;
546
+ }
547
+ throw new Error(result.error ?? `${result.stepName} failed`);
548
+ })
549
+ );
550
+
551
+ stepResult.status = "completed";
552
+ stepResult.completedAt = new Date();
553
+ stepResult.extractedData = {
554
+ waitFor,
555
+ winner: winner.stepName,
556
+ launched: step.steps.length,
557
+ };
558
+
559
+ workflowDb.updateStep(stepId, {
560
+ status: "completed",
561
+ completedAt: stepResult.completedAt,
562
+ result: JSON.stringify(stepResult.extractedData),
563
+ });
564
+
565
+ return stepResult;
566
+ } catch {
567
+ const settled = await Promise.all(branchPromises);
568
+ const failed = settled.filter((result) => result.status === "failed");
569
+ throw new Error(
570
+ `Parallel step ${step.name} failed: no branch completed successfully (${failed
571
+ .map((branch) => branch.stepName)
572
+ .join(", ")})`
573
+ );
574
+ }
575
+ }
576
+
577
+ // waitFor === "all" (default)
578
+ const childResults = await Promise.all(branchPromises);
579
+ const completedCount = childResults.filter((result) => result.status === "completed").length;
580
+ const failed = childResults.filter((result) => result.status === "failed");
581
+
582
+ if (failed.length > 0) {
583
+ throw new Error(
584
+ `Parallel branches failed: ${failed
585
+ .map((branch) => `${branch.stepName} (${branch.error ?? "unknown error"})`)
586
+ .join(", ")}`
587
+ );
588
+ }
589
+
590
+ stepResult.status = "completed";
591
+ stepResult.completedAt = new Date();
592
+ stepResult.extractedData = {
593
+ waitFor,
594
+ completed: completedCount,
595
+ failed: failed.length,
596
+ };
597
+
598
+ workflowDb.updateStep(stepId, {
599
+ status: "completed",
600
+ completedAt: stepResult.completedAt,
601
+ result: JSON.stringify(stepResult.extractedData),
602
+ });
603
+
604
+ return stepResult;
605
+ } catch (error) {
606
+ stepResult.status = "failed";
607
+ stepResult.error = error instanceof Error ? error.message : String(error);
608
+ stepResult.completedAt = new Date();
609
+
610
+ workflowDb.updateStep(stepId, {
611
+ status: "failed",
612
+ errorMessage: stepResult.error,
613
+ completedAt: stepResult.completedAt,
614
+ });
615
+
616
+ return stepResult;
617
+ }
618
+ }
619
+
620
+ private async executeParallelBranch(
621
+ executionId: string,
622
+ childStep: WorkflowStep,
623
+ contextManager: ContextManager
624
+ ): Promise<{ stepName: string; status: "completed" | "failed"; error?: string }> {
625
+ try {
626
+ await this.executeStepWithErrorHandling(executionId, childStep, contextManager);
627
+ return { stepName: childStep.name, status: "completed" };
628
+ } catch (error) {
629
+ return {
630
+ stepName: childStep.name,
631
+ status: "failed",
632
+ error: error instanceof Error ? error.message : String(error),
633
+ };
634
+ }
635
+ }
636
+
637
+ private async executeLoopStep(
638
+ executionId: string,
639
+ step: LoopStep,
640
+ contextManager: ContextManager
641
+ ): Promise<StepResult> {
642
+ const execution = this.executions.get(executionId);
643
+ if (!execution) {
644
+ throw new Error(`Execution ${executionId} not found`);
645
+ }
646
+
647
+ const workflowDb = createWorkflowDb(this.database);
648
+ const stepResult: StepResult = {
649
+ status: "running",
650
+ startedAt: new Date(),
651
+ };
652
+ execution.stepResults.set(step.name, stepResult);
653
+
654
+ const stepId = workflowDb.createStep({
655
+ executionId,
656
+ stepName: step.name,
657
+ status: "running",
658
+ });
659
+
660
+ const previousContext = contextManager.getAllContext();
661
+
662
+ try {
663
+ const items = this.resolveLoopItems(step.items, contextManager);
664
+ const iterationStepNames: string[] = [];
665
+
666
+ for (let index = 0; index < items.length; index++) {
667
+ const item = items[index];
668
+ contextManager.set("item", item);
669
+ contextManager.set("index", index);
670
+
671
+ const iterationStep: WorkflowStep = {
672
+ ...step.step,
673
+ name: `${step.step.name}[${index}]`,
674
+ };
675
+ iterationStepNames.push(iterationStep.name);
676
+
677
+ await this.executeStepWithErrorHandling(executionId, iterationStep, contextManager);
678
+ }
679
+
680
+ this.setStepContextValue(contextManager, execution, step.name, "results", items);
681
+
682
+ stepResult.status = "completed";
683
+ stepResult.completedAt = new Date();
684
+ stepResult.extractedData = {
685
+ iterations: items.length,
686
+ stepNames: iterationStepNames,
687
+ };
688
+
689
+ workflowDb.updateStep(stepId, {
690
+ status: "completed",
691
+ completedAt: stepResult.completedAt,
692
+ result: JSON.stringify(stepResult.extractedData),
693
+ });
694
+
695
+ return stepResult;
696
+ } catch (error) {
697
+ stepResult.status = "failed";
698
+ stepResult.error = error instanceof Error ? error.message : String(error);
699
+ stepResult.completedAt = new Date();
700
+
701
+ workflowDb.updateStep(stepId, {
702
+ status: "failed",
703
+ errorMessage: stepResult.error,
704
+ completedAt: stepResult.completedAt,
705
+ });
706
+
707
+ return stepResult;
708
+ } finally {
709
+ if ("item" in previousContext) {
710
+ contextManager.set("item", previousContext.item);
711
+ } else {
712
+ contextManager.set("item", undefined);
713
+ }
714
+
715
+ if ("index" in previousContext) {
716
+ contextManager.set("index", previousContext.index);
717
+ } else {
718
+ contextManager.set("index", undefined);
719
+ }
720
+ }
721
+ }
722
+
723
+ private async executeLogStep(
724
+ executionId: string,
725
+ step: LogStep,
726
+ contextManager: ContextManager
727
+ ): Promise<StepResult> {
728
+ const execution = this.executions.get(executionId);
729
+ if (!execution) {
730
+ throw new Error(`Execution ${executionId} not found`);
731
+ }
732
+
733
+ const workflowDb = createWorkflowDb(this.database);
734
+ const stepResult: StepResult = {
735
+ status: "running",
736
+ startedAt: new Date(),
737
+ };
738
+ execution.stepResults.set(step.name, stepResult);
739
+
740
+ const stepId = workflowDb.createStep({
741
+ executionId,
742
+ stepName: step.name,
743
+ status: "running",
744
+ });
745
+
746
+ try {
747
+ const message = contextManager.substitute(step.message);
748
+
749
+ stepResult.status = "completed";
750
+ stepResult.completedAt = new Date();
751
+ stepResult.extractedData = { message };
752
+
753
+ workflowDb.updateStep(stepId, {
754
+ status: "completed",
755
+ completedAt: stepResult.completedAt,
756
+ result: JSON.stringify(stepResult.extractedData),
757
+ });
758
+
759
+ return stepResult;
760
+ } catch (error) {
761
+ stepResult.status = "failed";
762
+ stepResult.error = error instanceof Error ? error.message : String(error);
763
+ stepResult.completedAt = new Date();
764
+
765
+ workflowDb.updateStep(stepId, {
766
+ status: "failed",
767
+ errorMessage: stepResult.error,
768
+ completedAt: stepResult.completedAt,
769
+ });
770
+
771
+ return stepResult;
772
+ }
773
+ }
774
+
775
+ private evaluateCondition(condition: string, contextManager: ContextManager): boolean {
776
+ const substituted = contextManager.substitute(condition).trim();
777
+ if (!substituted) {
778
+ return false;
779
+ }
780
+
781
+ if (substituted.includes("${")) {
782
+ throw new Error(`Unresolved variable in condition: ${condition}`);
783
+ }
784
+
785
+ const match = substituted.match(/^(.*?)(===|!==|==|!=|>=|<=|>|<)(.*)$/);
786
+ if (!match) {
787
+ return this.toBoolean(this.parseConditionValue(substituted));
788
+ }
789
+
790
+ const leftRaw = match[1];
791
+ const operator = match[2];
792
+ const rightRaw = match[3];
793
+ if (leftRaw === undefined || operator === undefined || rightRaw === undefined) {
794
+ return false;
795
+ }
796
+ const left = this.parseConditionValue(leftRaw);
797
+ const right = this.parseConditionValue(rightRaw);
798
+
799
+ switch (operator) {
800
+ case "===":
801
+ return left === right;
802
+ case "!==":
803
+ return left !== right;
804
+ case "==":
805
+ return this.areConditionValuesEqual(left, right);
806
+ case "!=":
807
+ return !this.areConditionValuesEqual(left, right);
808
+ case ">":
809
+ return this.compareValues(left, right) > 0;
810
+ case "<":
811
+ return this.compareValues(left, right) < 0;
812
+ case ">=":
813
+ return this.compareValues(left, right) >= 0;
814
+ case "<=":
815
+ return this.compareValues(left, right) <= 0;
816
+ default:
817
+ return false;
818
+ }
819
+ }
820
+
821
+ private parseConditionValue(raw: string): unknown {
822
+ const value = raw.trim();
823
+ if (value === "") {
824
+ return "";
825
+ }
826
+
827
+ const quotedMatch = value.match(/^(['"])(.*)\1$/);
828
+ if (quotedMatch) {
829
+ return quotedMatch[2];
830
+ }
831
+
832
+ if (value === "true") return true;
833
+ if (value === "false") return false;
834
+ if (value === "null") return null;
835
+ if (value === "undefined") return undefined;
836
+
837
+ const num = Number(value);
838
+ if (!Number.isNaN(num)) {
839
+ return num;
840
+ }
841
+
842
+ return value;
843
+ }
844
+
845
+ private compareValues(left: unknown, right: unknown): number {
846
+ if (typeof left === "number" && typeof right === "number") {
847
+ return left - right;
848
+ }
849
+
850
+ return String(left).localeCompare(String(right));
851
+ }
852
+
853
+ private areConditionValuesEqual(left: unknown, right: unknown): boolean {
854
+ if (left === right) {
855
+ return true;
856
+ }
857
+
858
+ if (typeof left === "number" && typeof right === "string") {
859
+ const parsed = Number(right);
860
+ return !Number.isNaN(parsed) && left === parsed;
861
+ }
862
+
863
+ if (typeof left === "string" && typeof right === "number") {
864
+ const parsed = Number(left);
865
+ return !Number.isNaN(parsed) && parsed === right;
866
+ }
867
+
868
+ return String(left) === String(right);
869
+ }
870
+
871
+ private toBoolean(value: unknown): boolean {
872
+ if (typeof value === "boolean") return value;
873
+ if (typeof value === "number") return value !== 0;
874
+ if (typeof value === "string") {
875
+ const normalized = value.trim().toLowerCase();
876
+ return normalized !== "" && normalized !== "false" && normalized !== "0";
877
+ }
878
+ return Boolean(value);
879
+ }
880
+
881
+ private resolveLoopItems(itemsExpr: string, contextManager: ContextManager): unknown[] {
882
+ const exactVariableMatch = itemsExpr.trim().match(/^\$\{([^}]+)\}$/);
883
+ let resolved: unknown;
884
+
885
+ if (exactVariableMatch) {
886
+ const variablePath = exactVariableMatch[1];
887
+ resolved = variablePath ? contextManager.get(variablePath.trim()) : undefined;
888
+ } else {
889
+ resolved = contextManager.substitute(itemsExpr);
890
+ }
891
+
892
+ if (Array.isArray(resolved)) {
893
+ return resolved;
894
+ }
895
+
896
+ if (typeof resolved === "string") {
897
+ const trimmed = resolved.trim();
898
+ if (!trimmed) {
899
+ return [];
900
+ }
901
+
902
+ try {
903
+ const parsed = JSON.parse(trimmed);
904
+ if (Array.isArray(parsed)) {
905
+ return parsed;
906
+ }
907
+ } catch {
908
+ // Fall through to comma-delimited values.
909
+ }
910
+
911
+ if (trimmed.includes(",")) {
912
+ return trimmed
913
+ .split(",")
914
+ .map((item) => item.trim())
915
+ .filter((item) => item.length > 0);
916
+ }
917
+
918
+ return [trimmed];
919
+ }
920
+
921
+ throw new Error(`Foreach step expected array-like items, got ${typeof resolved}`);
922
+ }
923
+
924
+ private setStepContextValue(
925
+ contextManager: ContextManager,
926
+ execution: WorkflowExecution,
927
+ stepName: string,
928
+ key: string,
929
+ value: unknown
930
+ ): void {
931
+ const steps = contextManager.get("steps");
932
+ const stepsObj: Record<string, any> =
933
+ steps && typeof steps === "object" && !Array.isArray(steps) ? steps : {};
934
+ const stepObj: Record<string, any> =
935
+ stepsObj[stepName] &&
936
+ typeof stepsObj[stepName] === "object" &&
937
+ !Array.isArray(stepsObj[stepName])
938
+ ? stepsObj[stepName]
939
+ : {};
940
+
941
+ stepObj[key] = value;
942
+ stepsObj[stepName] = stepObj;
943
+ contextManager.set("steps", stepsObj);
944
+
945
+ const executionSteps: Record<string, any> =
946
+ execution.context.steps &&
947
+ typeof execution.context.steps === "object" &&
948
+ !Array.isArray(execution.context.steps)
949
+ ? execution.context.steps
950
+ : {};
951
+ const executionStepObj: Record<string, any> =
952
+ executionSteps[stepName] &&
953
+ typeof executionSteps[stepName] === "object" &&
954
+ !Array.isArray(executionSteps[stepName])
955
+ ? executionSteps[stepName]
956
+ : {};
957
+
958
+ executionStepObj[key] = value;
959
+ executionSteps[stepName] = executionStepObj;
960
+ execution.context.steps = executionSteps;
961
+
962
+ // Keep dotted-path compatibility for older callers.
963
+ execution.context[`steps.${stepName}.${key}`] = value;
964
+ }
965
+
966
+ private sleep(ms: number): Promise<void> {
967
+ return new Promise((resolve) => setTimeout(resolve, ms));
968
+ }
969
+ }