@qualitas-id/mcp 1.0.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/constants.d.ts +18 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +18 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +82 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/schemas/common.d.ts +20 -0
  12. package/dist/schemas/common.d.ts.map +1 -0
  13. package/dist/schemas/common.js +10 -0
  14. package/dist/schemas/common.js.map +1 -0
  15. package/dist/schemas/flow.d.ts +358 -0
  16. package/dist/schemas/flow.d.ts.map +1 -0
  17. package/dist/schemas/flow.js +72 -0
  18. package/dist/schemas/flow.js.map +1 -0
  19. package/dist/schemas/project.d.ts +70 -0
  20. package/dist/schemas/project.d.ts.map +1 -0
  21. package/dist/schemas/project.js +26 -0
  22. package/dist/schemas/project.js.map +1 -0
  23. package/dist/schemas/run.d.ts +54 -0
  24. package/dist/schemas/run.d.ts.map +1 -0
  25. package/dist/schemas/run.js +20 -0
  26. package/dist/schemas/run.js.map +1 -0
  27. package/dist/schemas/variable.d.ts +91 -0
  28. package/dist/schemas/variable.d.ts.map +1 -0
  29. package/dist/schemas/variable.js +30 -0
  30. package/dist/schemas/variable.js.map +1 -0
  31. package/dist/services/api-client.d.ts +101 -0
  32. package/dist/services/api-client.d.ts.map +1 -0
  33. package/dist/services/api-client.js +184 -0
  34. package/dist/services/api-client.js.map +1 -0
  35. package/dist/services/flow-generator.d.ts +31 -0
  36. package/dist/services/flow-generator.d.ts.map +1 -0
  37. package/dist/services/flow-generator.js +638 -0
  38. package/dist/services/flow-generator.js.map +1 -0
  39. package/dist/shared-types.d.ts +579 -0
  40. package/dist/shared-types.d.ts.map +1 -0
  41. package/dist/shared-types.js +12 -0
  42. package/dist/shared-types.js.map +1 -0
  43. package/dist/tools/flows.d.ts +13 -0
  44. package/dist/tools/flows.d.ts.map +1 -0
  45. package/dist/tools/flows.js +458 -0
  46. package/dist/tools/flows.js.map +1 -0
  47. package/dist/tools/projects.d.ts +13 -0
  48. package/dist/tools/projects.d.ts.map +1 -0
  49. package/dist/tools/projects.js +381 -0
  50. package/dist/tools/projects.js.map +1 -0
  51. package/dist/tools/runs.d.ts +9 -0
  52. package/dist/tools/runs.d.ts.map +1 -0
  53. package/dist/tools/runs.js +342 -0
  54. package/dist/tools/runs.js.map +1 -0
  55. package/dist/tools/utils.d.ts +12 -0
  56. package/dist/tools/utils.d.ts.map +1 -0
  57. package/dist/tools/utils.js +144 -0
  58. package/dist/tools/utils.js.map +1 -0
  59. package/dist/tools/variables.d.ts +9 -0
  60. package/dist/tools/variables.d.ts.map +1 -0
  61. package/dist/tools/variables.js +316 -0
  62. package/dist/tools/variables.js.map +1 -0
  63. package/dist/types.d.ts +117 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +8 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist/utils/flow-layout.d.ts +34 -0
  68. package/dist/utils/flow-layout.d.ts.map +1 -0
  69. package/dist/utils/flow-layout.js +109 -0
  70. package/dist/utils/flow-layout.js.map +1 -0
  71. package/dist/utils/flow-validation.d.ts +74 -0
  72. package/dist/utils/flow-validation.d.ts.map +1 -0
  73. package/dist/utils/flow-validation.js +386 -0
  74. package/dist/utils/flow-validation.js.map +1 -0
  75. package/dist/utils/ocr.d.ts +25 -0
  76. package/dist/utils/ocr.d.ts.map +1 -0
  77. package/dist/utils/ocr.js +88 -0
  78. package/dist/utils/ocr.js.map +1 -0
  79. package/package.json +65 -0
  80. package/skills/qualitas.md +253 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Flow Generator Service
3
+ *
4
+ * Generates Qualitas flow graphs from natural language scenarios.
5
+ * Maps scenario steps to appropriate node types and builds the
6
+ * full FlowNode/FlowEdge/Variable structure ready for the API.
7
+ */
8
+ // ===========================================
9
+ // NODE_REFERENCE - Comprehensive documentation of all 40 FlowNodeType values
10
+ // Embedded in the MCP tool description so the AI knows what nodes are available.
11
+ // ===========================================
12
+ export const NODE_REFERENCE = `## Available Flow Node Types (40 types)
13
+
14
+ Use these node types when constructing flows. Each node has a \`type\` and a \`data\` object with type-specific fields.
15
+ Fields marked **required** must be provided. All others are optional.
16
+
17
+ Variable references: Use \`{{project.VAR_NAME}}\` for project variables, \`{{flow.VAR_NAME}}\` for flow variables.
18
+ Selectors: Use CSS selectors (e.g., \`.btn-primary\`, \`#email-input\`, \`[data-testid="submit"]\`).
19
+
20
+ ---
21
+
22
+ ### Triggers
23
+
24
+ | Type | Description | Required Fields | Optional Fields |
25
+ |------|-------------|-----------------|-----------------|
26
+ | \`start\` | Entry point of the flow (always first node) | _(none)_ | \`label\`, \`description\`, \`timeout\` |
27
+ | \`schedule\` | Cron-based scheduled trigger | \`cron\` (string, e.g. "0 9 * * 1-5") | \`description\`, \`timeout\` |
28
+
29
+ ### Navigation
30
+
31
+ | Type | Description | Required Fields | Optional Fields |
32
+ |------|-------------|-----------------|-----------------|
33
+ | \`navigate\` | Navigate to a URL | \`url\` (string) | \`waitUntil\` ("load" \\| "domcontentloaded" \\| "networkidle"), \`description\`, \`timeout\` |
34
+ | \`reload\` | Reload the current page | _(none)_ | \`waitUntil\`, \`description\`, \`timeout\` |
35
+ | \`goBack\` | Browser back navigation | _(none)_ | \`description\`, \`timeout\` |
36
+ | \`goForward\` | Browser forward navigation | _(none)_ | \`description\`, \`timeout\` |
37
+
38
+ ### Mouse Actions
39
+
40
+ | Type | Description | Required Fields | Optional Fields |
41
+ |------|-------------|-----------------|-----------------|
42
+ | \`click\` | Click an element | \`selector\` (string) | \`button\` ("left" \\| "right" \\| "middle"), \`clickCount\`, \`position\` ({x,y}), \`description\`, \`timeout\` |
43
+ | \`dblclick\` | Double-click an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
44
+ | \`rightclick\` | Right-click an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
45
+ | \`hover\` | Hover over an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
46
+ | \`dragAndDrop\` | Drag from source to target | \`sourceSelector\`, \`targetSelector\` | \`description\`, \`timeout\` |
47
+
48
+ ### Input
49
+
50
+ | Type | Description | Required Fields | Optional Fields |
51
+ |------|-------------|-----------------|-----------------|
52
+ | \`fill\` | Fill a text input (replaces content) | \`selector\`, \`text\` | \`description\`, \`timeout\` |
53
+ | \`type\` | Type text character by character | \`selector\`, \`text\` | \`delay\` (ms between keystrokes), \`description\`, \`timeout\` |
54
+ | \`clear\` | Clear an input field | \`selector\` | \`description\`, \`timeout\` |
55
+ | \`press\` | Press a keyboard key | \`key\` (e.g. "Enter", "Tab", "Escape", "ArrowDown") | \`selector\` (focus first), \`modifiers\` (array of "ctrl"/"alt"/"shift"/"meta"), \`description\`, \`timeout\` |
56
+
57
+ ### Form
58
+
59
+ | Type | Description | Required Fields | Optional Fields |
60
+ |------|-------------|-----------------|-----------------|
61
+ | \`submit\` | Submit a form | \`selector\` | \`description\`, \`timeout\` |
62
+ | \`check\` | Check a checkbox | \`selector\` | \`description\`, \`timeout\` |
63
+ | \`uncheck\` | Uncheck a checkbox | \`selector\` | \`description\`, \`timeout\` |
64
+ | \`select\` | Select dropdown option | \`selector\`, \`value\` | \`byLabel\` (boolean - select by visible label), \`description\`, \`timeout\` |
65
+ | \`upload\` | Upload a file | \`selector\`, \`filePath\` | \`description\`, \`timeout\`. \`filePath\` can be: local path, URL (http/https), or base64 data URL |
66
+
67
+ ### Focus / Scroll
68
+
69
+ | Type | Description | Required Fields | Optional Fields |
70
+ |------|-------------|-----------------|-----------------|
71
+ | \`focus\` | Focus an element | \`selector\` | \`description\`, \`timeout\` |
72
+ | \`blur\` | Remove focus from element | \`selector\` | \`description\`, \`timeout\` |
73
+ | \`scroll\` | Scroll the page or element | _(none)_ | \`selector\`, \`x\`, \`y\`, \`behavior\` ("auto" \\| "smooth"), \`direction\` ("up"/"down"/"left"/"right"), \`amount\`, \`description\`, \`timeout\` |
74
+ | \`scrollIntoView\` | Scroll element into view | \`selector\` | \`behavior\`, \`description\`, \`timeout\` |
75
+
76
+ ### Wait / Sync
77
+
78
+ | Type | Description | Required Fields | Optional Fields |
79
+ |------|-------------|-----------------|-----------------|
80
+ | \`wait\` | Wait for a duration | \`duration\` (ms) | \`description\`, \`timeout\` |
81
+ | \`waitForElement\` | Wait for element state | \`selector\` | \`state\` ("visible" \\| "hidden" \\| "attached" \\| "detached"), \`description\`, \`timeout\` |
82
+ | \`waitForNavigation\` | Wait for page navigation | _(none)_ | \`url\` (URL pattern), \`description\`, \`timeout\` |
83
+ | \`waitForResponse\` | Wait for network response | \`urlPattern\` (string) | \`method\`, \`statusCode\`, \`description\`, \`timeout\` |
84
+
85
+ ### Assertions
86
+
87
+ | Type | Description | Required Fields | Optional Fields |
88
+ |------|-------------|-----------------|-----------------|
89
+ | \`assertVisible\` | Assert element visibility | \`selector\` | \`state\` ("visible" \\| "hidden"), \`description\`, \`timeout\` |
90
+ | \`assertText\` | Assert element text content | \`selector\`, \`expectedText\` | \`exact\` (boolean), \`description\`, \`timeout\` |
91
+ | \`assertValue\` | Assert input value | \`selector\`, \`expectedValue\` | \`description\`, \`timeout\` |
92
+ | \`assertUrl\` | Assert current URL | \`expectedUrl\` | \`exact\` (boolean), \`description\`, \`timeout\` |
93
+ | \`assertTitle\` | Assert page title | \`expectedTitle\` | \`exact\` (boolean), \`description\`, \`timeout\` |
94
+ | \`assertVariable\` | Assert variable comparison | \`leftValue\`, \`operator\`, \`rightValue\` | \`description\`, \`timeout\`. Operators: "equals", "notEquals", "contains", "greaterThan", "lessThan" |
95
+ | \`assertTableCell\` | Assert table cell value | \`tableSelector\`, \`findColumnName\`, \`findOperator\`, \`findValue\`, \`assertColumnName\`, \`assertOperator\`, \`expectedValue\` | \`description\`, \`timeout\` |
96
+ | \`assertCount\` | Assert element count | \`selector\`, \`operator\`, \`expectedValue\` (number) | \`description\`, \`timeout\`. Operators: "equals", "greaterThan", "lessThan" |
97
+
98
+ ### Utility
99
+
100
+ | Type | Description | Required Fields | Optional Fields |
101
+ |------|-------------|-----------------|-----------------|
102
+ | \`screenshot\` | Take a screenshot | _(none)_ | \`name\`, \`fullPage\` (boolean), \`description\`, \`timeout\` |
103
+ | \`extractFromElement\` | Extract data from element | \`selector\`, \`extractType\`, \`saveAs\` | \`attributeName\`, \`trim\`, \`toNumber\`, \`regex\`, \`replace\`, \`description\`, \`timeout\`. extractType: "text" \\| "value" \\| "attribute" \\| "count" |
104
+ | \`ifCondition\` | Conditional branching | \`saveResultAs\` | \`conditionType\` ("variable" \\| "element"), \`leftValue\`, \`operator\`, \`rightValue\`, \`selector\`, \`description\`, \`timeout\` |
105
+ | \`loop\` | Loop over items | \`loopVariableName\` | \`loopType\` ("list" \\| "elements"), \`dataSource\`, \`selector\`, \`description\`, \`timeout\` |
106
+ | \`executeScript\` | Run custom JavaScript | \`script\` (string) | \`description\`, \`timeout\` |
107
+
108
+ ### Integration
109
+
110
+ | Type | Description | Required Fields | Optional Fields |
111
+ |------|-------------|-----------------|-----------------|
112
+ | \`apiCall\` | Make an HTTP request | \`method\`, \`url\` | \`headers\` (array of {key, value, enabled}), \`body\`, \`extractions\` (array of {id, variableName, type, expression}), \`description\`, \`timeout\` |
113
+ | \`generateEmail\` | Generate a test email | \`outputKey\` | \`emailName\`, \`retentionMinutes\`, \`provider\`, \`localPartPrefix\`, \`description\`, \`timeout\` |
114
+ | \`readEmail\` | Read from email inbox | \`inboxId\`, \`outputKey\` | \`generatedFrom\`, \`subjectContains\`, \`bodyContains\`, \`fromContains\`, \`timeoutMs\`, \`pollIntervalMs\`, \`description\` |
115
+ | \`extractEmail\` | Extract data from email | \`source\`, \`variableName\`, \`pattern\` | \`readSourceNodeId\`, \`sourceField\`, \`extractionMode\`, \`flags\`, \`matchIndex\`, \`selector\`, \`selectorTarget\`, \`attributeName\`, \`description\`, \`timeout\` |
116
+
117
+ ### Reusable
118
+
119
+ | Type | Description | Required Fields | Optional Fields |
120
+ |------|-------------|-----------------|-----------------|
121
+ | \`callFragment\` | Call a reusable flow fragment | \`fragmentId\` | \`fragmentName\`, \`description\`, \`timeout\` |
122
+ `;
123
+ // ===========================================
124
+ // Scenario Parsing Helpers
125
+ // ===========================================
126
+ /** Map of keywords to node type detection */
127
+ const NAVIGATION_KEYWORDS = [
128
+ "navigate", "go to", "open", "visit", "browse to", "load", "url",
129
+ ];
130
+ const CLICK_KEYWORDS = [
131
+ "click", "tap", "press button", "select button", "hit", "push",
132
+ ];
133
+ const DBLCLICK_KEYWORDS = [
134
+ "double click", "double-click", "dblclick",
135
+ ];
136
+ const RIGHTCLICK_KEYWORDS = [
137
+ "right click", "right-click", "rightclick",
138
+ ];
139
+ const HOVER_KEYWORDS = [
140
+ "hover", "mouse over", "mouse-over", "mouseover",
141
+ ];
142
+ const FILL_KEYWORDS = [
143
+ "fill", "enter", "input", "type into", "type in", "fill in", "fill out",
144
+ "write", "provide", "specify",
145
+ ];
146
+ const SELECT_KEYWORDS = [
147
+ "select", "choose", "pick", "dropdown",
148
+ ];
149
+ const CHECK_KEYWORDS = [
150
+ "check", "tick", "enable", "turn on",
151
+ ];
152
+ const UNCHECK_KEYWORDS = [
153
+ "uncheck", "untick", "disable", "turn off",
154
+ ];
155
+ const SUBMIT_KEYWORDS = [
156
+ "submit", "send form", "post form",
157
+ ];
158
+ const ASSERT_VISIBLE_KEYWORDS = [
159
+ "verify", "assert", "check that", "should see", "should display",
160
+ "expect", "confirm", "validate", "visible", "appears",
161
+ ];
162
+ const ASSERT_TEXT_KEYWORDS = [
163
+ "text should be", "text is", "shows", "displays", "contains text",
164
+ "should say", "should read", "message",
165
+ ];
166
+ const WAIT_KEYWORDS = [
167
+ "wait", "pause", "delay", "sleep",
168
+ ];
169
+ const SCREENSHOT_KEYWORDS = [
170
+ "screenshot", "capture", "take screenshot", "take a screenshot",
171
+ ];
172
+ const SCROLL_KEYWORDS = [
173
+ "scroll", "scroll down", "scroll up", "scroll to",
174
+ ];
175
+ const LOGIN_KEYWORDS = [
176
+ "log in", "login", "sign in", "signin", "authenticate",
177
+ ];
178
+ const NAVIGATE_URL_PATTERN = /(?:navigate|go|open|visit|browse)\s+(?:to\s+)?(https?:\/\/[^\s,;]+)/i;
179
+ const URL_PATTERN = /(https?:\/\/[^\s,;]+)/i;
180
+ const SELECTOR_PATTERN = /[.#][\w-]+|\[data-testid=["'][^"']+["']\]|(?:button|input|form|a|div|span|p|h[1-6]|select|textarea|label)\[?[^\]]*\]?/i;
181
+ const VARIABLE_PATTERN = /\{\{(?:project|flow)\.[A-Z_]+\}\}/g;
182
+ const DURATION_PATTERN = /(\d+)\s*(?:ms|milliseconds?|seconds?|s)/i;
183
+ /**
184
+ * Extract a CSS selector from a step description.
185
+ * Tries explicit selectors first, then infers from common patterns.
186
+ */
187
+ function extractSelector(step) {
188
+ // Look for explicit CSS selectors
189
+ const selectorMatch = step.match(SELECTOR_PATTERN);
190
+ if (selectorMatch)
191
+ return selectorMatch[0];
192
+ // Look for data-testid patterns
193
+ const testIdMatch = step.match(/(?:data-testid|testid|test-id)[\s=:]+["']?([\w-]+)["']?/i);
194
+ if (testIdMatch)
195
+ return `[data-testid="${testIdMatch[1]}"]`;
196
+ // Look for quoted element descriptions -> convert to common selectors
197
+ const quotedMatch = step.match(/["']([^"']+)["']/);
198
+ if (quotedMatch) {
199
+ const text = quotedMatch[1];
200
+ // Check for button-like text
201
+ if (/\b(btn|button|submit|cancel|save|delete|add|remove|close)\b/i.test(text)) {
202
+ return `button:has-text("${text}")`;
203
+ }
204
+ // Check for link-like text
205
+ if (/\b(link|href)\b/i.test(step) || step.includes("link")) {
206
+ return `a:has-text("${text}")`;
207
+ }
208
+ // Default to text selector
209
+ return `text="${text}"`;
210
+ }
211
+ // Infer from common UI element mentions
212
+ if (/\bemail\b/i.test(step))
213
+ return 'input[type="email"], input[name="email"], #email';
214
+ if (/\bpassword\b/i.test(step))
215
+ return 'input[type="password"], input[name="password"], #password';
216
+ if (/\busername\b/i.test(step))
217
+ return 'input[name="username"], #username';
218
+ if (/\bsearch\b/i.test(step))
219
+ return 'input[type="search"], input[name="search"], #search';
220
+ if (/\bform\b/i.test(step))
221
+ return "form";
222
+ if (/\bsubmit\b/i.test(step))
223
+ return 'button[type="submit"], button:has-text("Submit")';
224
+ // Default fallback
225
+ return "body";
226
+ }
227
+ /**
228
+ * Extract a URL from a step description.
229
+ */
230
+ function extractUrl(step, variables) {
231
+ const navMatch = step.match(NAVIGATE_URL_PATTERN);
232
+ if (navMatch)
233
+ return navMatch[1];
234
+ const urlMatch = step.match(URL_PATTERN);
235
+ if (urlMatch)
236
+ return urlMatch[1];
237
+ // Check for variable references
238
+ const varMatches = step.match(VARIABLE_PATTERN);
239
+ if (varMatches && varMatches.length > 0)
240
+ return varMatches[0];
241
+ // Common URL patterns
242
+ if (/\bhomepage\b|\bhome page\b/i.test(step))
243
+ return "{{project.BASE_URL}}";
244
+ if (/\bdashboard\b/i.test(step))
245
+ return "{{project.BASE_URL}}/dashboard";
246
+ if (/\blogin\b|\bsign in\b/i.test(step))
247
+ return "{{project.BASE_URL}}/login";
248
+ return "{{project.BASE_URL}}";
249
+ }
250
+ /**
251
+ * Extract text to fill/type from a step description.
252
+ */
253
+ function extractText(step) {
254
+ // Look for quoted text
255
+ const quotedMatch = step.match(/(?:with|text|value|content|input)\s*[:=]?\s*["']([^"']+)["']/i);
256
+ if (quotedMatch)
257
+ return quotedMatch[1];
258
+ // Look for email patterns
259
+ const emailMatch = step.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
260
+ if (emailMatch)
261
+ return emailMatch[0];
262
+ // Look for variable references
263
+ const varMatch = step.match(/\{\{(?:project|flow)\.[A-Z_]+\}\}/);
264
+ if (varMatch)
265
+ return varMatch[0];
266
+ // Look for "with X" pattern
267
+ const withMatch = step.match(/with\s+(?:the\s+)?(?:text\s+)?["']?([^"']+?)["']?\s*(?:in|on|into|to|$)/i);
268
+ if (withMatch)
269
+ return withMatch[1].trim();
270
+ return "";
271
+ }
272
+ /**
273
+ * Extract a duration in ms from a step description.
274
+ */
275
+ function extractDuration(step) {
276
+ const match = step.match(DURATION_PATTERN);
277
+ if (!match)
278
+ return 1000;
279
+ const value = parseInt(match[1], 10);
280
+ const unit = match[0].toLowerCase();
281
+ if (unit.includes("ms") || unit.includes("millisecond"))
282
+ return value;
283
+ return value * 1000; // seconds -> ms
284
+ }
285
+ /**
286
+ * Extract expected text from assertion steps.
287
+ */
288
+ function extractExpectedText(step) {
289
+ const quotedMatch = step.match(/(?:should|expect|assert|verify|contains?|shows?|displays?)\s*[:=]?\s*["']([^"']+)["']/i);
290
+ if (quotedMatch)
291
+ return quotedMatch[1];
292
+ const textMatch = step.match(/text\s*[:=]?\s*["']([^"']+)["']/i);
293
+ if (textMatch)
294
+ return textMatch[1];
295
+ return "";
296
+ }
297
+ /**
298
+ * Classify a step and return the appropriate node type + data.
299
+ */
300
+ function classifyStep(step, variables) {
301
+ const lower = step.toLowerCase().trim();
302
+ // --- Navigation ---
303
+ if (NAVIGATION_KEYWORDS.some((kw) => lower.includes(kw)) || URL_PATTERN.test(step)) {
304
+ return {
305
+ type: "navigate",
306
+ data: { url: extractUrl(step, variables), waitUntil: "load" },
307
+ };
308
+ }
309
+ // --- Login (composite pattern -> fill email, fill password, click submit) ---
310
+ // We handle this as a single navigate + fill + click in parseScenario
311
+ // --- Double Click ---
312
+ if (DBLCLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
313
+ return { type: "dblclick", data: { selector: extractSelector(step) } };
314
+ }
315
+ // --- Right Click ---
316
+ if (RIGHTCLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
317
+ return { type: "rightclick", data: { selector: extractSelector(step) } };
318
+ }
319
+ // --- Hover ---
320
+ if (HOVER_KEYWORDS.some((kw) => lower.includes(kw))) {
321
+ return { type: "hover", data: { selector: extractSelector(step) } };
322
+ }
323
+ // --- Submit ---
324
+ if (SUBMIT_KEYWORDS.some((kw) => lower.includes(kw))) {
325
+ return { type: "submit", data: { selector: extractSelector(step) } };
326
+ }
327
+ // --- Check/Uncheck ---
328
+ if (UNCHECK_KEYWORDS.some((kw) => lower.includes(kw))) {
329
+ return { type: "uncheck", data: { selector: extractSelector(step) } };
330
+ }
331
+ if (CHECK_KEYWORDS.some((kw) => lower.includes(kw))) {
332
+ return { type: "check", data: { selector: extractSelector(step) } };
333
+ }
334
+ // --- Select/Dropdown ---
335
+ if (SELECT_KEYWORDS.some((kw) => lower.includes(kw)) && (lower.includes("option") || lower.includes("dropdown") || lower.includes("from"))) {
336
+ const value = extractText(step);
337
+ return { type: "select", data: { selector: extractSelector(step), value, byLabel: true } };
338
+ }
339
+ // --- Fill/Input ---
340
+ if (FILL_KEYWORDS.some((kw) => lower.includes(kw))) {
341
+ const text = extractText(step);
342
+ return { type: "fill", data: { selector: extractSelector(step), text } };
343
+ }
344
+ // --- Click ---
345
+ if (CLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
346
+ return { type: "click", data: { selector: extractSelector(step) } };
347
+ }
348
+ // --- Wait ---
349
+ if (WAIT_KEYWORDS.some((kw) => lower.includes(kw))) {
350
+ if (lower.includes("element") || lower.includes("appear") || lower.includes("visible")) {
351
+ return { type: "waitForElement", data: { selector: extractSelector(step), state: "visible" } };
352
+ }
353
+ if (lower.includes("navigation") || lower.includes("page load") || lower.includes("redirect")) {
354
+ return { type: "waitForNavigation", data: {} };
355
+ }
356
+ if (lower.includes("response") || lower.includes("api") || lower.includes("request")) {
357
+ return { type: "waitForResponse", data: { urlPattern: extractUrl(step, variables) || "**" } };
358
+ }
359
+ return { type: "wait", data: { duration: extractDuration(step) } };
360
+ }
361
+ // --- Screenshot ---
362
+ if (SCREENSHOT_KEYWORDS.some((kw) => lower.includes(kw))) {
363
+ const nameMatch = step.match(/(?:named?|called?)\s*["']([^"']+)["']/i);
364
+ return { type: "screenshot", data: { name: nameMatch?.[1], fullPage: lower.includes("full") } };
365
+ }
366
+ // --- Scroll ---
367
+ if (SCROLL_KEYWORDS.some((kw) => lower.includes(kw))) {
368
+ if (lower.includes("to") || lower.includes("into view") || lower.includes("element")) {
369
+ return { type: "scrollIntoView", data: { selector: extractSelector(step), behavior: "smooth" } };
370
+ }
371
+ const direction = lower.includes("up") ? "up" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "down";
372
+ return { type: "scroll", data: { direction, amount: 500 } };
373
+ }
374
+ // --- Assertions (after fill/click, so more specific matches take priority) ---
375
+ if (ASSERT_VISIBLE_KEYWORDS.some((kw) => lower.includes(kw))) {
376
+ // Check if it's a text assertion
377
+ const expectedText = extractExpectedText(step);
378
+ if (expectedText && (lower.includes("text") || lower.includes("shows") || lower.includes("displays") || lower.includes("contains") || lower.includes("says") || lower.includes("reads"))) {
379
+ return {
380
+ type: "assertText",
381
+ data: { selector: extractSelector(step), expectedText, exact: false },
382
+ };
383
+ }
384
+ // Check if it's a URL assertion
385
+ if (lower.includes("url") || lower.includes("redirected") || lower.includes("page is")) {
386
+ return { type: "assertUrl", data: { expectedUrl: extractUrl(step, variables) } };
387
+ }
388
+ // Check if it's a title assertion
389
+ if (lower.includes("title")) {
390
+ return { type: "assertTitle", data: { expectedTitle: expectedText || "" } };
391
+ }
392
+ // Default to visible assertion
393
+ return { type: "assertVisible", data: { selector: extractSelector(step), state: "visible" } };
394
+ }
395
+ // --- Upload ---
396
+ if (lower.includes("upload")) {
397
+ const fileMatch = step.match(/(?:file|path)\s*[:=]?\s*["']([^"']+)["']/i);
398
+ return { type: "upload", data: { selector: extractSelector(step), filePath: fileMatch?.[1] || "/tmp/upload.txt" } };
399
+ }
400
+ // --- Focus ---
401
+ if (lower.includes("focus")) {
402
+ return { type: "focus", data: { selector: extractSelector(step) } };
403
+ }
404
+ // --- Default: assume it's a click on something ---
405
+ return { type: "click", data: { selector: extractSelector(step) } };
406
+ }
407
+ /**
408
+ * Split a scenario description into discrete steps.
409
+ */
410
+ function parseScenarioSteps(scenario) {
411
+ // Normalize line endings and clean up
412
+ const normalized = scenario
413
+ .replace(/\r\n/g, "\n")
414
+ .replace(/\t/g, " ")
415
+ .trim();
416
+ const steps = [];
417
+ // Try splitting by numbered steps: "1. ", "2. ", etc.
418
+ const numberedSteps = normalized.split(/\n\s*\d+[.)]\s*/);
419
+ if (numberedSteps.length > 1) {
420
+ for (const s of numberedSteps) {
421
+ const trimmed = s.trim();
422
+ if (trimmed)
423
+ steps.push(trimmed);
424
+ }
425
+ return steps;
426
+ }
427
+ // Try splitting by bullet points: "- ", "* ", "• "
428
+ const bulletSteps = normalized.split(/\n\s*[-*•]\s+/);
429
+ if (bulletSteps.length > 1) {
430
+ for (const s of bulletSteps) {
431
+ const trimmed = s.trim();
432
+ if (trimmed)
433
+ steps.push(trimmed);
434
+ }
435
+ return steps;
436
+ }
437
+ // Try splitting by newlines
438
+ const lineSteps = normalized.split(/\n+/);
439
+ if (lineSteps.length > 1) {
440
+ for (const s of lineSteps) {
441
+ const trimmed = s.trim();
442
+ if (trimmed)
443
+ steps.push(trimmed);
444
+ }
445
+ return steps;
446
+ }
447
+ // Try splitting by "and then", "then", ". " connectors
448
+ const connectorSteps = normalized.split(/\s*(?:,\s*then|;\s*then|\.\s*then|,\s*and then|\.\s*And|\. Then|,\s*then|\.\s*Next|\. After|\. Finally)\s*/i);
449
+ if (connectorSteps.length > 1) {
450
+ for (const s of connectorSteps) {
451
+ const trimmed = s.trim();
452
+ if (trimmed)
453
+ steps.push(trimmed);
454
+ }
455
+ return steps;
456
+ }
457
+ // Fall back to sentence splitting by period
458
+ const sentences = normalized.split(/\.\s+/);
459
+ for (const s of sentences) {
460
+ const trimmed = s.trim();
461
+ if (trimmed)
462
+ steps.push(trimmed.endsWith(".") ? trimmed : trimmed);
463
+ }
464
+ return steps;
465
+ }
466
+ /**
467
+ * Detect if a step describes a login pattern (composite action).
468
+ * Returns sub-steps if it's a login, or null otherwise.
469
+ */
470
+ function detectLoginPattern(step) {
471
+ const lower = step.toLowerCase();
472
+ const isLogin = LOGIN_KEYWORDS.some((kw) => lower.includes(kw));
473
+ if (!isLogin)
474
+ return null;
475
+ const subSteps = [];
476
+ // Extract email/username
477
+ const emailMatch = step.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
478
+ const usernameMatch = step.match(/(?:user\s*name|username|email)\s*[:=]?\s*["']?([^"',\s]+)["']?/i);
479
+ // Extract password
480
+ const passwordMatch = step.match(/(?:password|pwd|pass)\s*[:=]?\s*["']?([^"',\s]+)["']?/i);
481
+ // Determine login page URL
482
+ const urlMatch = step.match(URL_PATTERN);
483
+ if (urlMatch) {
484
+ subSteps.push(`Navigate to ${urlMatch[1]}`);
485
+ }
486
+ else {
487
+ subSteps.push("Navigate to login page");
488
+ }
489
+ const identifier = emailMatch?.[0] || usernameMatch?.[1] || "{{flow.USERNAME}}";
490
+ const password = passwordMatch?.[1] || "{{flow.PASSWORD}}";
491
+ subSteps.push(`Fill email/username field with "${identifier}"`);
492
+ subSteps.push(`Fill password field with "${password}"`);
493
+ subSteps.push("Click the login/submit button");
494
+ return subSteps;
495
+ }
496
+ /**
497
+ * Detect if a step describes a form-fill pattern (composite action).
498
+ */
499
+ function detectFormFillPattern(step) {
500
+ const lower = step.toLowerCase();
501
+ if (!lower.includes("form") && !lower.includes("fill out") && !lower.includes("complete"))
502
+ return null;
503
+ // Extract field:value pairs
504
+ const fieldPattern = /(\w+)\s*[:=]\s*["']([^"']+)["']/gi;
505
+ const fields = [];
506
+ let match;
507
+ while ((match = fieldPattern.exec(step)) !== null) {
508
+ fields.push({ name: match[1], value: match[2] });
509
+ }
510
+ if (fields.length === 0)
511
+ return null;
512
+ return fields.map((f) => `Fill ${f.name} field with "${f.value}"`);
513
+ }
514
+ // ===========================================
515
+ // Main Generator
516
+ // ===========================================
517
+ /**
518
+ * Generate a complete flow graph from a natural language scenario.
519
+ *
520
+ * @param params.scenario - Natural language description of the test steps
521
+ * @param params.name - Optional flow name (auto-generated from scenario if not provided)
522
+ * @param params.environment - Target environment (dev/staging/prod)
523
+ * @returns Generated flow with nodes, edges, and variables
524
+ */
525
+ export function generateFlowFromScenario(params) {
526
+ const { scenario, name, environment = "dev" } = params;
527
+ // Parse scenario into steps
528
+ let rawSteps = parseScenarioSteps(scenario);
529
+ // Expand composite patterns (login, form fill)
530
+ const expandedSteps = [];
531
+ for (const step of rawSteps) {
532
+ const loginSteps = detectLoginPattern(step);
533
+ if (loginSteps) {
534
+ expandedSteps.push(...loginSteps);
535
+ continue;
536
+ }
537
+ const formSteps = detectFormFillPattern(step);
538
+ if (formSteps) {
539
+ expandedSteps.push(...formSteps);
540
+ continue;
541
+ }
542
+ expandedSteps.push(step);
543
+ }
544
+ // Extract variables referenced in the scenario
545
+ const variables = [];
546
+ const varSet = new Set();
547
+ const allVarMatches = scenario.match(VARIABLE_PATTERN);
548
+ if (allVarMatches) {
549
+ for (const v of allVarMatches) {
550
+ const varName = v.replace(/\{\{(?:project|flow)\./, "").replace(/\}\}/, "");
551
+ if (!varSet.has(varName)) {
552
+ varSet.add(varName);
553
+ variables.push({
554
+ id: `var_${variables.length + 1}`,
555
+ name: varName,
556
+ scope: v.startsWith("{{project.") ? "project" : "flow",
557
+ values: { dev: "", staging: "", prod: "" },
558
+ });
559
+ }
560
+ }
561
+ }
562
+ // Auto-detect common variables that might be needed
563
+ const lowerScenario = scenario.toLowerCase();
564
+ if ((lowerScenario.includes("email") || lowerScenario.includes("login") || lowerScenario.includes("sign in")) &&
565
+ !varSet.has("USERNAME") && !varSet.has("EMAIL")) {
566
+ variables.push({
567
+ id: `var_${variables.length + 1}`,
568
+ name: "USERNAME",
569
+ scope: "flow",
570
+ values: { dev: "", staging: "", prod: "" },
571
+ });
572
+ }
573
+ if ((lowerScenario.includes("password") || lowerScenario.includes("login") || lowerScenario.includes("sign in")) &&
574
+ !varSet.has("PASSWORD")) {
575
+ variables.push({
576
+ id: `var_${variables.length + 1}`,
577
+ name: "PASSWORD",
578
+ scope: "flow",
579
+ values: { dev: "", staging: "", prod: "" },
580
+ });
581
+ }
582
+ // Build nodes
583
+ const nodes = [];
584
+ const edges = [];
585
+ const X_CENTER = 250;
586
+ const Y_SPACING = 100;
587
+ // Always start with a start node
588
+ nodes.push({
589
+ id: "node_1",
590
+ type: "start",
591
+ position: { x: X_CENTER, y: 0 },
592
+ data: { label: name || "Generated Flow" },
593
+ });
594
+ // Generate nodes for each step
595
+ for (let i = 0; i < expandedSteps.length; i++) {
596
+ const step = expandedSteps[i];
597
+ const nodeId = `node_${i + 2}`; // +2 because start is node_1
598
+ const { type, data } = classifyStep(step, variables);
599
+ nodes.push({
600
+ id: nodeId,
601
+ type,
602
+ position: { x: X_CENTER, y: (i + 1) * Y_SPACING },
603
+ data: { ...data, description: step },
604
+ });
605
+ }
606
+ // Generate sequential edges
607
+ for (let i = 0; i < nodes.length - 1; i++) {
608
+ edges.push({
609
+ id: `edge_${i + 1}`,
610
+ source: nodes[i].id,
611
+ target: nodes[i + 1].id,
612
+ });
613
+ }
614
+ // Auto-generate flow name from scenario if not provided
615
+ const flowName = name || generateFlowName(scenario);
616
+ return {
617
+ name: flowName,
618
+ nodes,
619
+ edges,
620
+ variables,
621
+ };
622
+ }
623
+ /**
624
+ * Generate a concise flow name from the scenario text.
625
+ */
626
+ function generateFlowName(scenario) {
627
+ // Take first ~60 chars and clean up
628
+ let name = scenario
629
+ .replace(/\n/g, " ")
630
+ .replace(/\s+/g, " ")
631
+ .trim();
632
+ if (name.length > 60) {
633
+ name = name.substring(0, 57) + "...";
634
+ }
635
+ // Capitalize first letter
636
+ return name.charAt(0).toUpperCase() + name.slice(1);
637
+ }
638
+ //# sourceMappingURL=flow-generator.js.map