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,83 @@
1
+ /**
2
+ * ContextManager - manages workflow context and variable substitution
3
+ */
4
+
5
+ export class ContextManager {
6
+ private context: Record<string, any>;
7
+
8
+ constructor() {
9
+ this.context = {};
10
+ }
11
+
12
+ /**
13
+ * Set a context value
14
+ * @param key - Context key
15
+ * @param value - Value to store
16
+ */
17
+ set(key: string, value: any): void {
18
+ this.context[key] = value;
19
+ }
20
+
21
+ /**
22
+ * Get a context value, supporting nested paths
23
+ * @param key - Context key or path (e.g., "user.name")
24
+ * @returns Value or undefined if not found
25
+ */
26
+ get(key: string): any {
27
+ const parts = key.split(".");
28
+ let current = this.context;
29
+
30
+ for (const part of parts) {
31
+ if (current === null || current === undefined) {
32
+ return undefined;
33
+ }
34
+
35
+ if (typeof current === "object" && part in current) {
36
+ current = current[part];
37
+ } else {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ return current;
43
+ }
44
+
45
+ /**
46
+ * Substitute variables in a string using ${variable} syntax
47
+ * @param text - Text with variables to substitute
48
+ * @returns Text with variables replaced
49
+ */
50
+ substitute(text: string): string {
51
+ // Match ${variable} or ${nested.path}
52
+ return text.replace(/\$\{([^}]+)\}/g, (match, key) => {
53
+ const value = this.get(key.trim());
54
+
55
+ if (value === undefined) {
56
+ // Leave unresolved variables as-is
57
+ return match;
58
+ }
59
+
60
+ // Convert objects/arrays to JSON
61
+ if (typeof value === "object" && value !== null) {
62
+ return JSON.stringify(value);
63
+ }
64
+
65
+ return String(value);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Get all context as a plain object
71
+ * @returns Copy of entire context
72
+ */
73
+ getAllContext(): Record<string, any> {
74
+ return { ...this.context };
75
+ }
76
+
77
+ /**
78
+ * Clear all context
79
+ */
80
+ clear(): void {
81
+ this.context = {};
82
+ }
83
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Workflow DSL and Parser
3
+ *
4
+ * Exports for workflow parsing, validation, and type definitions
5
+ */
6
+
7
+ export { parseWorkflow, parseWorkflowFromYaml, substituteVariables } from "./workflowParser";
8
+ export type {
9
+ BaseStep,
10
+ ConditionalStep,
11
+ ErrorStrategy,
12
+ ExtractConfig,
13
+ ExtractType,
14
+ LogStep,
15
+ LoopStep,
16
+ ParallelStep,
17
+ RetryConfig,
18
+ SessionStep,
19
+ StepResult,
20
+ StepStatus,
21
+ StepType,
22
+ WaitCondition,
23
+ WaitConditionType,
24
+ WorkflowContext,
25
+ WorkflowDefinition,
26
+ WorkflowExecution,
27
+ WorkflowStatus,
28
+ WorkflowStep,
29
+ WorkflowValidationError,
30
+ } from "./workflowTypes";
31
+ export { validateWorkflow } from "./workflowValidator";
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ResultExtractor - extracts results from transcript events using regex or JSONPath
3
+ */
4
+
5
+ import { JSONPath } from "jsonpath-plus";
6
+ import type { Database } from "../db/db.ts";
7
+ import type { ExtractConfig } from "./workflowTypes.ts";
8
+
9
+ export class ResultExtractor {
10
+ private db: Database;
11
+
12
+ constructor(db: Database) {
13
+ this.db = db;
14
+ }
15
+
16
+ /**
17
+ * Extract result from transcript events
18
+ * @param sessionId - Session to extract from
19
+ * @param config - Extraction configuration
20
+ * @returns Extracted value or null if not found
21
+ */
22
+ async extract(sessionId: string, config: ExtractConfig): Promise<any> {
23
+ // Validate extract type first
24
+ const configType = config.type as string;
25
+ if (configType !== "regex" && configType !== "jsonpath") {
26
+ throw new Error(`Unknown extract type: ${configType}`);
27
+ }
28
+
29
+ // Get all transcript events for session
30
+ const events = this.db.getEventsBySessionId(sessionId, {
31
+ source: "transcript",
32
+ });
33
+
34
+ // Concatenate all content
35
+ const allContent = events
36
+ .map((e) => {
37
+ const payload = e.payload as any;
38
+ return payload?.content;
39
+ })
40
+ .filter((content) => content !== null && content !== undefined)
41
+ .join("");
42
+
43
+ if (!allContent) {
44
+ return null;
45
+ }
46
+
47
+ switch (configType) {
48
+ case "regex":
49
+ return this.extractRegex(allContent, config);
50
+
51
+ case "jsonpath":
52
+ return this.extractJsonPath(allContent, config);
53
+
54
+ default:
55
+ throw new Error(`Unknown extract type: ${configType}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Extract using regex pattern
61
+ */
62
+ private extractRegex(content: string, config: ExtractConfig): any {
63
+ if (!config.pattern) {
64
+ throw new Error("pattern is required for regex extraction");
65
+ }
66
+
67
+ const match = content.match(new RegExp(config.pattern));
68
+ if (!match) {
69
+ return null;
70
+ }
71
+
72
+ // If there are capture groups, return them
73
+ if (match.length > 2) {
74
+ // Multiple capture groups: return array excluding full match
75
+ return match.slice(1);
76
+ }
77
+
78
+ if (match.length === 2) {
79
+ // Single capture group: return just the captured value
80
+ return match[1];
81
+ }
82
+
83
+ // No capture groups: return full match
84
+ return match[0];
85
+ }
86
+
87
+ /**
88
+ * Extract using JSONPath
89
+ */
90
+ private extractJsonPath(content: string, config: ExtractConfig): any {
91
+ if (!config.path) {
92
+ throw new Error("path is required for jsonpath extraction");
93
+ }
94
+
95
+ try {
96
+ // Parse content as JSON
97
+ const parsed = JSON.parse(content);
98
+
99
+ // Apply JSONPath query
100
+ const results = JSONPath({ path: config.path, json: parsed });
101
+
102
+ if (results.length === 0) {
103
+ return null;
104
+ }
105
+
106
+ // Return single result if only one match
107
+ if (results.length === 1) {
108
+ return results[0];
109
+ }
110
+
111
+ // Return array if multiple matches
112
+ return results;
113
+ } catch (error) {
114
+ const errorMsg = error instanceof Error ? error.message : String(error);
115
+ throw new Error(`Failed to parse JSON for JSONPath extraction: ${errorMsg}`);
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * WaitConditionPoller - polls database for wait condition satisfaction
3
+ */
4
+
5
+ import type { Database } from "../db/db.ts";
6
+ import type { WaitCondition } from "./workflowTypes.ts";
7
+
8
+ export class WaitConditionPoller {
9
+ private db: Database;
10
+ private pollInterval: number;
11
+
12
+ constructor(db: Database, pollInterval: number = 100) {
13
+ this.db = db;
14
+ this.pollInterval = pollInterval;
15
+ }
16
+
17
+ /**
18
+ * Wait for any of the given conditions to be satisfied
19
+ * @param sessionId - Session to monitor
20
+ * @param conditions - Array of conditions to check
21
+ * @throws Error if timeout occurs or conditions array is empty
22
+ */
23
+ async wait(sessionId: string, conditions: WaitCondition[]): Promise<void> {
24
+ if (conditions.length === 0) {
25
+ throw new Error("At least one wait condition is required");
26
+ }
27
+
28
+ const startTime = Date.now();
29
+ const minTimeout = this.getMinTimeout(conditions);
30
+
31
+ while (true) {
32
+ // Check timeout
33
+ if (minTimeout !== Infinity && Date.now() - startTime > minTimeout) {
34
+ throw new Error(`Wait condition timeout after ${minTimeout}ms for session ${sessionId}`);
35
+ }
36
+
37
+ // Check each condition
38
+ for (const condition of conditions) {
39
+ const satisfied = await this.checkCondition(sessionId, condition);
40
+ if (satisfied) {
41
+ return; // Condition met, resolve
42
+ }
43
+ }
44
+
45
+ // Poll interval delay
46
+ await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get minimum timeout from all conditions
52
+ */
53
+ private getMinTimeout(conditions: WaitCondition[]): number {
54
+ const timeouts = conditions.map((c) => c.timeout ?? Infinity).filter((t) => t !== undefined);
55
+
56
+ return timeouts.length > 0 ? Math.min(...timeouts) : Infinity;
57
+ }
58
+
59
+ /**
60
+ * Check if a specific condition is satisfied
61
+ */
62
+ private async checkCondition(sessionId: string, condition: WaitCondition): Promise<boolean> {
63
+ switch (condition.type) {
64
+ case "idle_prompt":
65
+ return this.checkNotification(sessionId, "idle_prompt");
66
+
67
+ case "permission_prompt":
68
+ return this.checkNotification(sessionId, "permission_prompt");
69
+
70
+ case "stop":
71
+ return this.checkStopEvent(sessionId);
72
+
73
+ case "event":
74
+ if (!condition.eventType) {
75
+ throw new Error("eventType is required for event wait condition");
76
+ }
77
+ return this.checkCustomEvent(sessionId, condition.eventType);
78
+
79
+ case "timeout":
80
+ // Timeout is handled separately in the main loop
81
+ return false;
82
+
83
+ default:
84
+ throw new Error(`Unknown wait condition type: ${(condition as any).type}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if a notification of given type exists
90
+ */
91
+ private checkNotification(sessionId: string, notificationType: string): boolean {
92
+ const events = this.db.getEventsBySessionId(sessionId, {
93
+ type: "Notification",
94
+ limit: 10, // Check recent events
95
+ });
96
+
97
+ // Check if any event has matching notification type
98
+ for (const event of events) {
99
+ const payload = event.payload as any;
100
+ if (payload?.type === notificationType) {
101
+ return true;
102
+ }
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ /**
109
+ * Check if Stop event exists
110
+ */
111
+ private checkStopEvent(sessionId: string): boolean {
112
+ const events = this.db.getEventsBySessionId(sessionId, {
113
+ type: "Stop",
114
+ limit: 1,
115
+ });
116
+
117
+ return events.length > 0;
118
+ }
119
+
120
+ /**
121
+ * Check if custom event exists
122
+ */
123
+ private checkCustomEvent(sessionId: string, eventType: string): boolean {
124
+ const events = this.db.getEventsBySessionId(sessionId, {
125
+ type: eventType,
126
+ limit: 1,
127
+ });
128
+
129
+ return events.length > 0;
130
+ }
131
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Workflow Parser
3
+ *
4
+ * Parses workflow definitions from YAML/JSON and provides variable substitution
5
+ */
6
+
7
+ import * as yaml from "js-yaml";
8
+ import type { WorkflowContext, WorkflowDefinition } from "./workflowTypes";
9
+
10
+ /**
11
+ * Parse a workflow definition from JSON string
12
+ *
13
+ * @param json - JSON string containing workflow definition
14
+ * @returns Parsed workflow or null if invalid
15
+ */
16
+ export function parseWorkflow(json: string): WorkflowDefinition | null {
17
+ try {
18
+ const data = JSON.parse(json);
19
+ return validateAndNormalizeWorkflow(data);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Parse a workflow definition from YAML string
27
+ *
28
+ * @param yamlString - YAML string containing workflow definition
29
+ * @returns Parsed workflow or null if invalid
30
+ */
31
+ export function parseWorkflowFromYaml(yamlString: string): WorkflowDefinition | null {
32
+ try {
33
+ const data = yaml.load(yamlString);
34
+ return validateAndNormalizeWorkflow(data);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Validate and normalize workflow data
42
+ *
43
+ * Ensures the parsed data conforms to WorkflowDefinition interface
44
+ */
45
+ function validateAndNormalizeWorkflow(data: unknown): WorkflowDefinition | null {
46
+ if (!data || typeof data !== "object") {
47
+ return null;
48
+ }
49
+
50
+ const obj = data as Record<string, unknown>;
51
+
52
+ // Required fields
53
+ if (typeof obj.name !== "string" || !obj.name) {
54
+ return null;
55
+ }
56
+
57
+ if (!Array.isArray(obj.steps)) {
58
+ return null;
59
+ }
60
+
61
+ // Build workflow definition
62
+ const workflow: WorkflowDefinition = {
63
+ name: obj.name,
64
+ steps: normalizeWorkflowSteps(obj.steps),
65
+ };
66
+
67
+ // Optional fields
68
+ if (typeof obj.description === "string") {
69
+ workflow.description = obj.description;
70
+ }
71
+
72
+ if (obj.env && typeof obj.env === "object") {
73
+ workflow.env = obj.env as Record<string, string>;
74
+ }
75
+
76
+ if (typeof obj.cwd === "string") {
77
+ workflow.cwd = obj.cwd;
78
+ }
79
+
80
+ return workflow;
81
+ }
82
+
83
+ function normalizeWorkflowSteps(steps: unknown[]): any[] {
84
+ return steps.map((step) => normalizeWorkflowStep(step));
85
+ }
86
+
87
+ function normalizeWorkflowStep(step: unknown): any {
88
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
89
+ return step;
90
+ }
91
+
92
+ const normalized: Record<string, any> = { ...(step as Record<string, unknown>) };
93
+
94
+ // Accept snake_case alias used by built-in YAML examples.
95
+ if (normalized.waitFor === undefined && normalized.wait_for !== undefined) {
96
+ normalized.waitFor = normalized.wait_for;
97
+ }
98
+
99
+ if (normalized.type === "parallel" && Array.isArray(normalized.steps)) {
100
+ normalized.steps = normalizeWorkflowSteps(normalized.steps);
101
+ }
102
+
103
+ if (normalized.type === "if") {
104
+ if (Array.isArray(normalized.then)) {
105
+ // biome-ignore lint/suspicious/noThenProperty: Workflow DSL uses "then" for conditional branches.
106
+ normalized.then = normalizeWorkflowSteps(normalized.then);
107
+ }
108
+
109
+ if (Array.isArray(normalized.else)) {
110
+ normalized.else = normalizeWorkflowSteps(normalized.else);
111
+ }
112
+ }
113
+
114
+ if (normalized.type === "foreach" && normalized.step) {
115
+ normalized.step = normalizeWorkflowStep(normalized.step);
116
+ }
117
+
118
+ return normalized;
119
+ }
120
+
121
+ /**
122
+ * Substitute variables in a string using workflow context
123
+ *
124
+ * Supports ${variable} and ${steps.stepName.field} syntax
125
+ *
126
+ * @param template - String containing variable placeholders
127
+ * @param context - Workflow context with variables and step results
128
+ * @returns String with variables substituted
129
+ */
130
+ export function substituteVariables(template: string, context: WorkflowContext): string {
131
+ // Match ${...} patterns (but not \${...})
132
+ const variablePattern = /(?<!\\)\$\{([^}]+)\}/g;
133
+
134
+ return template.replace(variablePattern, (match, path) => {
135
+ const value = resolveVariablePath(path.trim(), context);
136
+
137
+ if (value === undefined) {
138
+ // Keep original if not found
139
+ return match;
140
+ }
141
+
142
+ return valueToString(value);
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Resolve a variable path in the context
148
+ *
149
+ * Supports:
150
+ * - Direct variables: "name" -> context.variables.name
151
+ * - Step results: "steps.stepName.field" -> context.steps.stepName.field
152
+ * - Nested paths: "steps.stepName.extractedData.key"
153
+ *
154
+ * @param path - Dot-separated path to variable
155
+ * @param context - Workflow context
156
+ * @returns Resolved value or undefined
157
+ */
158
+ function resolveVariablePath(path: string, context: WorkflowContext): unknown {
159
+ const parts = path.split(".");
160
+
161
+ // Check if it starts with "steps"
162
+ if (parts[0] === "steps") {
163
+ // Navigate through context.steps
164
+ let current: unknown = context.steps;
165
+
166
+ for (let i = 1; i < parts.length; i++) {
167
+ if (!current || typeof current !== "object") {
168
+ return undefined;
169
+ }
170
+ const part = parts[i];
171
+ if (part === undefined) {
172
+ return undefined;
173
+ }
174
+ current = (current as Record<string, unknown>)[part];
175
+ }
176
+
177
+ return current;
178
+ }
179
+
180
+ // Otherwise, look in variables
181
+ let current: unknown = context.variables;
182
+
183
+ for (const part of parts) {
184
+ if (!current || typeof current !== "object") {
185
+ return undefined;
186
+ }
187
+ current = (current as Record<string, unknown>)[part];
188
+ }
189
+
190
+ return current;
191
+ }
192
+
193
+ /**
194
+ * Convert a value to string for substitution
195
+ *
196
+ * - Primitives: toString()
197
+ * - Objects/Arrays: JSON.stringify()
198
+ *
199
+ * @param value - Value to convert
200
+ * @returns String representation
201
+ */
202
+ function valueToString(value: unknown): string {
203
+ if (value === null || value === undefined) {
204
+ return String(value);
205
+ }
206
+
207
+ if (typeof value === "string") {
208
+ return value;
209
+ }
210
+
211
+ if (typeof value === "number" || typeof value === "boolean") {
212
+ return String(value);
213
+ }
214
+
215
+ // Objects and arrays -> JSON
216
+ return JSON.stringify(value);
217
+ }