slapify 0.0.14 → 0.0.17

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 (48) hide show
  1. package/README.md +342 -258
  2. package/dist/ai/interpreter.d.ts +13 -0
  3. package/dist/ai/interpreter.js +1 -293
  4. package/dist/browser/agent.js +1 -485
  5. package/dist/cli.js +1 -1315
  6. package/dist/config/loader.js +1 -305
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +1 -260
  9. package/dist/parser/flow.js +1 -117
  10. package/dist/perf/audit.d.ts +215 -0
  11. package/dist/perf/audit.js +1 -0
  12. package/dist/report/generator.d.ts +1 -0
  13. package/dist/report/generator.js +1 -549
  14. package/dist/runner/index.d.ts +14 -1
  15. package/dist/runner/index.js +1 -584
  16. package/dist/task/index.d.ts +5 -0
  17. package/dist/task/index.js +1 -0
  18. package/dist/task/report.d.ts +9 -0
  19. package/dist/task/report.js +1 -0
  20. package/dist/task/runner.d.ts +3 -0
  21. package/dist/task/runner.js +1 -0
  22. package/dist/task/session.d.ts +18 -0
  23. package/dist/task/session.js +1 -0
  24. package/dist/task/tools.d.ts +253 -0
  25. package/dist/task/tools.js +1 -0
  26. package/dist/task/types.d.ts +153 -0
  27. package/dist/task/types.js +1 -0
  28. package/dist/types.d.ts +2 -0
  29. package/dist/types.js +1 -2
  30. package/package.json +25 -15
  31. package/dist/ai/interpreter.d.ts.map +0 -1
  32. package/dist/ai/interpreter.js.map +0 -1
  33. package/dist/browser/agent.d.ts.map +0 -1
  34. package/dist/browser/agent.js.map +0 -1
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js.map +0 -1
  37. package/dist/config/loader.d.ts.map +0 -1
  38. package/dist/config/loader.js.map +0 -1
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/index.js.map +0 -1
  41. package/dist/parser/flow.d.ts.map +0 -1
  42. package/dist/parser/flow.js.map +0 -1
  43. package/dist/report/generator.d.ts.map +0 -1
  44. package/dist/report/generator.js.map +0 -1
  45. package/dist/runner/index.d.ts.map +0 -1
  46. package/dist/runner/index.js.map +0 -1
  47. package/dist/types.d.ts.map +0 -1
  48. package/dist/types.js.map +0 -1
@@ -1,584 +1 @@
1
- import { BrowserAgent } from "../browser/agent.js";
2
- import { AIInterpreter } from "../ai/interpreter.js";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import * as yaml from "yaml";
6
- /**
7
- * Test runner that executes flow files
8
- */
9
- export class TestRunner {
10
- config;
11
- credentials;
12
- browser;
13
- ai;
14
- autoHandled = [];
15
- allAssumptions = [];
16
- constructor(config, credentials) {
17
- this.config = config;
18
- this.credentials = credentials;
19
- this.browser = new BrowserAgent(config.browser);
20
- this.ai = new AIInterpreter(config.llm);
21
- }
22
- /**
23
- * Run a flow file
24
- * @param flow The flow file to run
25
- * @param onStep Optional callback for real-time step progress
26
- */
27
- async runFlow(flow, onStep) {
28
- const startTime = new Date();
29
- const stepResults = [];
30
- try {
31
- for (const step of flow.steps) {
32
- let result = await this.executeStep(step);
33
- // Retry once on failure (for non-optional steps, but not @debug_wait)
34
- const isDebugWait = step.text.match(/@debug_wait/i);
35
- if (result.status === "failed" && !step.optional && !isDebugWait) {
36
- // Wait a bit before retry
37
- await this.browser.wait(1000);
38
- // Try again
39
- const retryResult = await this.executeStep(step);
40
- retryResult.retried = true;
41
- // Combine durations
42
- retryResult.duration += result.duration;
43
- // If retry succeeded or failed, use retry result
44
- result = retryResult;
45
- }
46
- stepResults.push(result);
47
- // Call progress callback
48
- if (onStep) {
49
- onStep(result);
50
- }
51
- // Stop on required step failure (after retry)
52
- if (result.status === "failed" && !step.optional) {
53
- break;
54
- }
55
- }
56
- }
57
- finally {
58
- await this.browser.close();
59
- }
60
- const endTime = new Date();
61
- const passed = stepResults.filter((r) => r.status === "passed").length;
62
- const failed = stepResults.filter((r) => r.status === "failed").length;
63
- const skipped = stepResults.filter((r) => r.status === "skipped").length;
64
- return {
65
- flowFile: flow.path || flow.name,
66
- status: failed === 0 ||
67
- stepResults.every((r) => r.status !== "failed" ||
68
- flow.steps[stepResults.indexOf(r)]?.optional)
69
- ? "passed"
70
- : "failed",
71
- steps: stepResults,
72
- totalSteps: flow.steps.length,
73
- passedSteps: passed,
74
- failedSteps: failed,
75
- skippedSteps: skipped,
76
- duration: endTime.getTime() - startTime.getTime(),
77
- startTime,
78
- endTime,
79
- autoHandled: this.autoHandled,
80
- assumptions: this.allAssumptions,
81
- };
82
- }
83
- /**
84
- * Execute a single step
85
- */
86
- async executeStep(step) {
87
- const startTime = Date.now();
88
- const actions = [];
89
- const assumptions = [];
90
- try {
91
- // Get current browser state
92
- const state = await this.browser.getState();
93
- // Check for auto-handle opportunities
94
- await this.handleInterruptions(actions);
95
- // Check if this is a credential-related step
96
- // Supports multiple patterns:
97
- // - "Login with <profile> credentials"
98
- // - "@inject <profile>" or "@inject:<profile>"
99
- // - "Inject <profile> credentials"
100
- // - "Use <profile> credentials"
101
- // Profile names can contain letters, numbers, hyphens, and underscores
102
- const credentialPatterns = [
103
- /login\s+with\s+([\w-]+)\s+credentials/i,
104
- /@inject[:\s]+([\w-]+)/i,
105
- /inject\s+([\w-]+)\s+credentials/i,
106
- /use\s+([\w-]+)\s+credentials/i,
107
- ];
108
- let profileName = null;
109
- for (const pattern of credentialPatterns) {
110
- const match = step.text.match(pattern);
111
- if (match) {
112
- profileName = match[1];
113
- break;
114
- }
115
- }
116
- // Check for @debug_wait command
117
- const debugWaitMatch = step.text.match(/@debug_wait(?:[:\s]+(.+))?/i);
118
- if (debugWaitMatch) {
119
- const profileName = debugWaitMatch[1]?.trim() || "captured";
120
- await this.handleDebugWait(profileName, actions);
121
- return {
122
- step,
123
- status: "passed",
124
- duration: Date.now() - startTime,
125
- actions,
126
- };
127
- }
128
- if (profileName) {
129
- const profile = this.credentials.profiles[profileName];
130
- if (!profile) {
131
- throw new Error(`Credential profile not found: ${profileName}`);
132
- }
133
- await this.handleLogin(profile, actions);
134
- }
135
- else {
136
- // Interpret step with AI
137
- const interpreted = await this.ai.interpretStep(step, state, this.credentials.profiles);
138
- // Handle skip reason
139
- if (interpreted.skipReason) {
140
- if (step.optional || step.conditional) {
141
- return {
142
- step,
143
- status: "skipped",
144
- duration: Date.now() - startTime,
145
- actions: [
146
- {
147
- type: "info",
148
- description: interpreted.skipReason,
149
- timestamp: Date.now(),
150
- },
151
- ],
152
- };
153
- }
154
- else {
155
- throw new Error(interpreted.skipReason);
156
- }
157
- }
158
- // Record assumptions
159
- if (interpreted.assumptions.length > 0) {
160
- assumptions.push(...interpreted.assumptions);
161
- this.allAssumptions.push(...interpreted.assumptions);
162
- }
163
- // Execute browser commands
164
- for (const cmd of interpreted.actions) {
165
- await this.executeCommand(cmd, actions);
166
- }
167
- }
168
- // Take screenshot if enabled
169
- let screenshot;
170
- if (this.config.report?.screenshots) {
171
- const screenshotPath = `step-${step.line}.png`;
172
- await this.browser.screenshot(screenshotPath);
173
- screenshot = screenshotPath;
174
- }
175
- return {
176
- step,
177
- status: "passed",
178
- duration: Date.now() - startTime,
179
- actions,
180
- assumptions: assumptions.length > 0 ? assumptions : undefined,
181
- screenshot,
182
- };
183
- }
184
- catch (error) {
185
- // Take failure screenshot
186
- let screenshot;
187
- try {
188
- if (this.config.report?.screenshots) {
189
- const screenshotPath = `step-${step.line}-failed.png`;
190
- await this.browser.screenshot(screenshotPath);
191
- screenshot = screenshotPath;
192
- }
193
- }
194
- catch {
195
- // Ignore screenshot errors
196
- }
197
- if (step.optional) {
198
- return {
199
- step,
200
- status: "skipped",
201
- duration: Date.now() - startTime,
202
- actions,
203
- error: error.message,
204
- screenshot,
205
- };
206
- }
207
- return {
208
- step,
209
- status: "failed",
210
- duration: Date.now() - startTime,
211
- actions,
212
- error: error.message,
213
- screenshot,
214
- };
215
- }
216
- }
217
- /**
218
- * Execute a browser command
219
- */
220
- async executeCommand(cmd, actions) {
221
- actions.push({
222
- type: this.getActionType(cmd.command),
223
- description: cmd.description,
224
- selector: cmd.args[0],
225
- value: cmd.args[1],
226
- timestamp: Date.now(),
227
- });
228
- switch (cmd.command) {
229
- case "navigate":
230
- await this.browser.navigate(cmd.args[0]);
231
- // Wait for page to stabilize after navigation
232
- await this.browser.waitForStable();
233
- break;
234
- case "click":
235
- await this.browser.click(cmd.args[0]);
236
- // Brief wait after click in case it triggers navigation
237
- await this.browser.wait(300);
238
- break;
239
- case "fill":
240
- await this.browser.fill(cmd.args[0], cmd.args[1]);
241
- break;
242
- case "type":
243
- await this.browser.type(cmd.args[0], cmd.args[1]);
244
- break;
245
- case "press":
246
- await this.browser.press(cmd.args[0]);
247
- break;
248
- case "hover":
249
- await this.browser.hover(cmd.args[0]);
250
- break;
251
- case "select":
252
- await this.browser.select(cmd.args[0], cmd.args[1]);
253
- break;
254
- case "scroll":
255
- await this.browser.scroll(cmd.args[0], parseInt(cmd.args[1]));
256
- break;
257
- case "wait":
258
- await this.browser.wait(parseInt(cmd.args[0]));
259
- break;
260
- case "waitForText":
261
- await this.browser.wait(`text=${cmd.args[0]}`);
262
- break;
263
- case "getText":
264
- await this.browser.getText(cmd.args[0]);
265
- break;
266
- case "screenshot":
267
- await this.browser.screenshot(cmd.args[0]);
268
- break;
269
- case "goBack":
270
- await this.browser.goBack();
271
- break;
272
- case "reload":
273
- await this.browser.reload();
274
- break;
275
- default:
276
- throw new Error(`Unknown command: ${cmd.command}`);
277
- }
278
- // Small delay between actions for stability
279
- await this.browser.wait(100);
280
- }
281
- /**
282
- * Handle automatic dismissal of interruptions
283
- */
284
- async handleInterruptions(actions) {
285
- try {
286
- const state = await this.browser.getState();
287
- const interruptions = await this.ai.checkAutoHandle(state);
288
- for (const int of interruptions) {
289
- try {
290
- await this.browser.click(int.ref);
291
- const description = `Auto-handled: ${int.description}`;
292
- this.autoHandled.push(description);
293
- actions.push({
294
- type: "auto-handle",
295
- description,
296
- selector: int.ref,
297
- timestamp: Date.now(),
298
- });
299
- // Wait for any animations
300
- await this.browser.wait(500);
301
- }
302
- catch {
303
- // Ignore failed auto-handles
304
- }
305
- }
306
- }
307
- catch {
308
- // Ignore auto-handle errors
309
- }
310
- }
311
- /**
312
- * Handle login with credentials
313
- */
314
- async handleLogin(profile, actions) {
315
- // Handle inject type (cookies/localStorage)
316
- if (profile.type === "inject") {
317
- let successCount = 0;
318
- let failCount = 0;
319
- if (profile.cookies) {
320
- for (const cookie of profile.cookies) {
321
- try {
322
- await this.browser.setCookie(cookie.name, cookie.value);
323
- actions.push({
324
- type: "fill",
325
- description: `Set cookie: ${cookie.name}`,
326
- timestamp: Date.now(),
327
- });
328
- successCount++;
329
- }
330
- catch (error) {
331
- // Log but continue - some cookies may fail due to domain restrictions
332
- actions.push({
333
- type: "info",
334
- description: `⚠ Failed to set cookie: ${cookie.name} (${error.message?.split("\n")[0] || "unknown error"})`,
335
- timestamp: Date.now(),
336
- });
337
- failCount++;
338
- }
339
- }
340
- }
341
- if (profile.localStorage) {
342
- for (const [key, value] of Object.entries(profile.localStorage)) {
343
- try {
344
- await this.browser.setLocalStorage(key, value);
345
- actions.push({
346
- type: "fill",
347
- description: `Set localStorage: ${key}`,
348
- timestamp: Date.now(),
349
- });
350
- successCount++;
351
- }
352
- catch (error) {
353
- actions.push({
354
- type: "info",
355
- description: `⚠ Failed to set localStorage: ${key} (${error.message?.split("\n")[0] || "unknown error"})`,
356
- timestamp: Date.now(),
357
- });
358
- failCount++;
359
- }
360
- }
361
- }
362
- if (profile.sessionStorage) {
363
- for (const [key, value] of Object.entries(profile.sessionStorage)) {
364
- try {
365
- await this.browser.setSessionStorage(key, value);
366
- actions.push({
367
- type: "fill",
368
- description: `Set sessionStorage: ${key}`,
369
- timestamp: Date.now(),
370
- });
371
- successCount++;
372
- }
373
- catch (error) {
374
- actions.push({
375
- type: "info",
376
- description: `⚠ Failed to set sessionStorage: ${key} (${error.message?.split("\n")[0] || "unknown error"})`,
377
- timestamp: Date.now(),
378
- });
379
- failCount++;
380
- }
381
- }
382
- }
383
- if (failCount > 0) {
384
- console.log(` ⚠ ${failCount} item(s) failed to inject (continuing with ${successCount} successful)`);
385
- }
386
- // Reload so the page picks up the injected cookies/storage
387
- await this.browser.wait(300);
388
- await this.browser.reload();
389
- return;
390
- }
391
- // Handle login form type
392
- if (profile.type === "login-form" && profile.username && profile.password) {
393
- const state = await this.browser.getState();
394
- const loginForm = await this.ai.findLoginForm(state);
395
- if (!loginForm) {
396
- throw new Error("Could not find login form on page");
397
- }
398
- // Fill username
399
- await this.browser.fill(loginForm.usernameRef, profile.username);
400
- actions.push({
401
- type: "fill",
402
- description: "Filled username field",
403
- selector: loginForm.usernameRef,
404
- timestamp: Date.now(),
405
- });
406
- // Fill password
407
- await this.browser.fill(loginForm.passwordRef, profile.password);
408
- actions.push({
409
- type: "fill",
410
- description: "Filled password field",
411
- selector: loginForm.passwordRef,
412
- timestamp: Date.now(),
413
- });
414
- // Click submit
415
- await this.browser.click(loginForm.submitRef);
416
- actions.push({
417
- type: "click",
418
- description: "Clicked login button",
419
- selector: loginForm.submitRef,
420
- timestamp: Date.now(),
421
- });
422
- // Wait for navigation
423
- await this.browser.wait(2000);
424
- // Handle TOTP if configured
425
- if (profile.totp_secret) {
426
- const totp = this.generateTOTP(profile.totp_secret);
427
- // TODO: Find OTP input and fill
428
- actions.push({
429
- type: "fill",
430
- description: `Entered TOTP code`,
431
- timestamp: Date.now(),
432
- });
433
- }
434
- // Handle fixed OTP
435
- if (profile.fixed_otp) {
436
- // TODO: Find OTP input and fill
437
- actions.push({
438
- type: "fill",
439
- description: `Entered fixed OTP`,
440
- timestamp: Date.now(),
441
- });
442
- }
443
- }
444
- }
445
- /**
446
- * Generate TOTP code from secret
447
- */
448
- generateTOTP(secret) {
449
- // Basic TOTP implementation
450
- // In production, use a proper TOTP library
451
- const epoch = Math.floor(Date.now() / 1000 / 30);
452
- // Simplified - would need proper HMAC-SHA1 implementation
453
- return "000000"; // Placeholder
454
- }
455
- /**
456
- * Handle @debug_wait - pause for user interaction, then capture credentials
457
- */
458
- async handleDebugWait(profileName, actions) {
459
- console.log("\n" + "=".repeat(60));
460
- console.log("šŸ”“ DEBUG WAIT - Browser paused for manual interaction");
461
- console.log("=".repeat(60));
462
- console.log("\nYou can now interact with the browser manually.");
463
- console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n");
464
- console.log("Press ENTER when done to capture cookies & localStorage...\n");
465
- actions.push({
466
- type: "info",
467
- description: "Paused for manual interaction (@debug_wait)",
468
- timestamp: Date.now(),
469
- });
470
- // Wait for user input using raw stdin
471
- await new Promise((resolve) => {
472
- const onData = () => {
473
- process.stdin.removeListener("data", onData);
474
- process.stdin.pause();
475
- resolve();
476
- };
477
- process.stdin.resume();
478
- process.stdin.once("data", onData);
479
- });
480
- console.log("\nšŸ“ø Capturing browser state...\n");
481
- // Capture cookies
482
- const cookies = await this.browser.getCookies();
483
- let localStorage = await this.browser.getLocalStorage();
484
- let sessionStorage = await this.browser.getSessionStorage();
485
- // Normalize to plain Record<string, string> (in case we got a string or nested structure)
486
- const toStorageObject = (v) => {
487
- if (typeof v === "string") {
488
- try {
489
- v = JSON.parse(v);
490
- }
491
- catch {
492
- return {};
493
- }
494
- }
495
- if (!v || typeof v !== "object" || Array.isArray(v))
496
- return {};
497
- const out = {};
498
- for (const [k, val] of Object.entries(v)) {
499
- out[String(k)] = typeof val === "string" ? val : JSON.stringify(val);
500
- }
501
- return out;
502
- };
503
- localStorage = toStorageObject(localStorage);
504
- sessionStorage = toStorageObject(sessionStorage);
505
- // Build credential profile with correct shape for .slapify/credentials.yaml
506
- const capturedProfile = {
507
- type: "inject",
508
- };
509
- if (cookies.length > 0) {
510
- capturedProfile.cookies = cookies.map((c) => ({
511
- name: c.name,
512
- value: c.value,
513
- }));
514
- }
515
- if (Object.keys(localStorage).length > 0) {
516
- capturedProfile.localStorage = localStorage;
517
- }
518
- if (Object.keys(sessionStorage).length > 0) {
519
- capturedProfile.sessionStorage = sessionStorage;
520
- }
521
- // Save to temp_credentials.yaml (format compatible with .slapify/credentials.yaml)
522
- const outputPath = path.join(process.cwd(), "temp_credentials.yaml");
523
- let existingData = {
524
- profiles: {},
525
- };
526
- if (fs.existsSync(outputPath)) {
527
- try {
528
- const parsed = yaml.parse(fs.readFileSync(outputPath, "utf-8"));
529
- if (parsed && parsed.profiles && typeof parsed.profiles === "object") {
530
- existingData = { profiles: parsed.profiles };
531
- }
532
- }
533
- catch {
534
- existingData = { profiles: {} };
535
- }
536
- }
537
- existingData.profiles[profileName] = capturedProfile;
538
- // Stringify with block style so localStorage/sessionStorage are key: value per line
539
- const yamlContent = `# Captured credentials from @debug_wait
540
- # Generated: ${new Date().toISOString()}
541
- #
542
- # To use: copy the profile you need to .slapify/credentials.yaml
543
- # Then use: @inject ${profileName}
544
-
545
- ${yaml.stringify(existingData, { indent: 2, lineWidth: 0 })}`;
546
- fs.writeFileSync(outputPath, yamlContent);
547
- // Summary
548
- console.log("āœ… Captured:");
549
- console.log(` - ${cookies.length} cookie(s)`);
550
- console.log(` - ${Object.keys(localStorage).length} localStorage item(s)`);
551
- console.log(` - ${Object.keys(sessionStorage).length} sessionStorage item(s)`);
552
- console.log(`\nšŸ“ Saved to: ${outputPath}`);
553
- console.log(` Profile name: "${profileName}"`);
554
- console.log("\n" + "=".repeat(60) + "\n");
555
- actions.push({
556
- type: "info",
557
- description: `Captured ${cookies.length} cookies, ${Object.keys(localStorage).length} localStorage, ${Object.keys(sessionStorage).length} sessionStorage to temp_credentials.yaml`,
558
- timestamp: Date.now(),
559
- });
560
- }
561
- /**
562
- * Get action type from command
563
- */
564
- getActionType(command) {
565
- switch (command) {
566
- case "navigate":
567
- return "navigate";
568
- case "click":
569
- case "hover":
570
- case "press":
571
- return "click";
572
- case "fill":
573
- case "type":
574
- case "select":
575
- return "fill";
576
- case "wait":
577
- case "waitForText":
578
- return "wait";
579
- default:
580
- return "info";
581
- }
582
- }
583
- }
584
- //# sourceMappingURL=index.js.map
1
+ import{BrowserAgent as e}from"../browser/agent.js";import{AIInterpreter as t}from"../ai/interpreter.js";import*as s from"fs";import*as a from"path";import*as i from"yaml";export class TestRunner{config;credentials;browser;ai;autoHandled=[];allAssumptions=[];constructor(s,a){this.config=s,this.credentials=a,this.browser=new e(s.browser),this.ai=new t(s.llm)}async runFlow(e,t,s){const a=new Date,i=[];let o;try{for(const s of e.steps){let e=await this.executeStep(s);const a=s.text.match(/@debug_wait/i);if("failed"===e.status&&!s.optional&&!a){await this.browser.wait(1e3);const t=await this.executeStep(s);t.retried=!0,t.duration+=e.duration,e=t}if(i.push(e),t&&t(e),"failed"===e.status&&!s.optional)break}if(s)try{const e=await this.browser.getUrl(),{collectCoreWebVitals:t,collectReactScanResults:s}=await import("../perf/audit.js"),[a,i]=await Promise.all([t(this.browser),s(this.browser)]);o={url:e,auditedAt:(new Date).toISOString(),vitals:a,react:i,scores:null}}catch{}}finally{await this.browser.close()}if(s&&o)try{const{runLighthouseAudit:e}=await import("../perf/audit.js"),t=this.config.report?.output_dir||"./test-reports",s=await e(o.url,t);s&&(o.scores=s.scores,o.lighthouse=s.scores,s.reportPath&&(o.lighthouseReportPath=s.reportPath))}catch{}const r=new Date,n=i.filter(e=>"passed"===e.status).length,c=i.filter(e=>"failed"===e.status).length,l=i.filter(e=>"skipped"===e.status).length;return{flowFile:e.path||e.name,status:0===c||i.every(t=>"failed"!==t.status||e.steps[i.indexOf(t)]?.optional)?"passed":"failed",steps:i,totalSteps:e.steps.length,passedSteps:n,failedSteps:c,skippedSteps:l,duration:r.getTime()-a.getTime(),startTime:a,endTime:r,autoHandled:this.autoHandled,assumptions:this.allAssumptions,...o?{perfAudit:o}:{}}}async executeStep(e){const t=Date.now(),s=[],a=[];try{const i=await this.browser.getState();await this.handleInterruptions(s);const o=[/login\s+with\s+([\w-]+)\s+credentials/i,/@inject[:\s]+([\w-]+)/i,/inject\s+([\w-]+)\s+credentials/i,/use\s+([\w-]+)\s+credentials/i],r=[/^(log\s?in|sign\s?in|authenticate|login)(\s+to\s+\S+)?(\s+using\s+credentials?)?$/i,/^(log\s?in|sign\s?in)\s+with\s+credentials?$/i];let n=null;for(const t of o){const s=e.text.match(t);if(s){n=s[1];break}}if(!n){r.some(t=>t.test(e.text.trim()))&&(n=this.pickBestCredentialProfile(i.url))}const c=e.text.match(/@debug_wait(?:[:\s]+(.+))?/i);if(c){const a=c[1]?.trim()||"captured";return await this.handleDebugWait(a,s),{step:e,status:"passed",duration:Date.now()-t,actions:s}}if(n){const e=this.credentials.profiles[n];if(!e)throw new Error(`Credential profile not found: ${n}`);await this.handleLogin(e,s)}else{const o=await this.ai.interpretStep(e,i,this.credentials.profiles);if(o.needsCredentials&&!n){const a=o.credentialProfile,r=this.credentials?.profiles||{};if(n=a&&r[a]?a:this.pickBestCredentialProfile(i.url),n){await this.handleLogin(r[n],s);const a=this.config.report?.screenshots?await this.browser.screenshot(`step-${e.line}.png`).catch(()=>{}):void 0;return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:[`Auto-selected credential profile: ${n}`],screenshot:a}}}if(o.skipReason){if(e.optional||e.conditional)return{step:e,status:"skipped",duration:Date.now()-t,actions:[{type:"info",description:o.skipReason,timestamp:Date.now()}]};throw new Error(o.skipReason)}o.assumptions.length>0&&(a.push(...o.assumptions),this.allAssumptions.push(...o.assumptions));for(const e of o.actions)await this.executeCommand(e,s)}let l;if(await this.handleCaptcha(s),await this.handleInterruptions(s),this.config.report?.screenshots){const t=`step-${e.line}.png`;await this.browser.screenshot(t),l=t}return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:a.length>0?a:void 0,screenshot:l}}catch(a){let i;try{if(this.config.report?.screenshots){const t=`step-${e.line}-failed.png`;await this.browser.screenshot(t),i=t}}catch{}return e.optional?{step:e,status:"skipped",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}:{step:e,status:"failed",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}}}async executeCommand(e,t){switch(t.push({type:this.getActionType(e.command),description:e.description,selector:e.args[0],value:e.args[1],timestamp:Date.now()}),e.command){case"navigate":await this.browser.navigate(e.args[0]),await this.browser.waitForStable();break;case"click":await this.browser.click(e.args[0]),await this.browser.wait(300);break;case"fill":await this.browser.fill(e.args[0],e.args[1]);break;case"type":await this.browser.type(e.args[0],e.args[1]);break;case"press":await this.browser.press(e.args[0]);break;case"hover":await this.browser.hover(e.args[0]);break;case"select":await this.browser.select(e.args[0],e.args[1]);break;case"scroll":await this.browser.scroll(e.args[0],parseInt(e.args[1]));break;case"wait":await this.browser.wait(parseInt(e.args[0]));break;case"waitForText":await this.browser.wait(`text=${e.args[0]}`);break;case"getText":await this.browser.getText(e.args[0]);break;case"screenshot":await this.browser.screenshot(e.args[0]);break;case"goBack":await this.browser.goBack();break;case"reload":await this.browser.reload();break;default:throw new Error(`Unknown command: ${e.command}`)}await this.browser.wait(100)}pickBestCredentialProfile(e){const t=this.credentials?.profiles;if(!t||0===Object.keys(t).length)return null;try{const s=new URL(e).hostname.replace(/^www\./,""),a=s.split(".")[0];for(const e of Object.keys(t))if(e.toLowerCase()===a.toLowerCase()||e.toLowerCase()===s.toLowerCase())return e}catch{}return t.default?"default":Object.keys(t)[0]}async handleCaptcha(e){try{const t=await this.browser.getState(),s=(t.snapshot||"").toLowerCase(),a=(t.title||"").toLowerCase(),i=(t.url||"").toLowerCase();if(![s.includes("recaptcha"),s.includes("hcaptcha"),s.includes("turnstile"),s.includes("i'm not a robot"),s.includes("i am not a robot"),s.includes("verify you are human"),s.includes("verify you're human"),a.includes("just a moment"),i.includes("challenge")].some(Boolean))return;const o=await this.ai.findCaptchaAction(t);if(!o)return;await this.browser.click(o.ref),await this.browser.wait(2e3);const r=`Auto-solved captcha: ${o.description}`;this.autoHandled.push(r),e.push({type:"auto-handle",description:r,selector:o.ref,timestamp:Date.now()})}catch{}}async handleInterruptions(e){try{const t=await this.browser.getState(),s=await this.ai.checkAutoHandle(t);for(const t of s)try{await this.browser.click(t.ref);const s=`Auto-handled: ${t.description}`;this.autoHandled.push(s),e.push({type:"auto-handle",description:s,selector:t.ref,timestamp:Date.now()}),await this.browser.wait(500)}catch{}}catch{}}async handleLogin(e,t){if("inject"===e.type){let s=0,a=0;if(e.cookies)for(const i of e.cookies)try{await this.browser.setCookie(i.name,i.value),t.push({type:"fill",description:`Set cookie: ${i.name}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set cookie: ${i.name} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.localStorage)for(const[i,o]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(i,o),t.push({type:"fill",description:`Set localStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set localStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.sessionStorage)for(const[i,o]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(i,o),t.push({type:"fill",description:`Set sessionStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set sessionStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}return a>0&&console.log(` ⚠ ${a} item(s) failed to inject (continuing with ${s} successful)`),await this.browser.wait(300),void await this.browser.reload()}if("login-form"===e.type&&e.username&&e.password){const s=await this.browser.getState(),a=await this.ai.findLoginForm(s);if(!a)throw new Error("Could not find login form on page");if(await this.browser.fill(a.usernameRef,e.username),t.push({type:"fill",description:"Filled username field",selector:a.usernameRef,timestamp:Date.now()}),await this.browser.fill(a.passwordRef,e.password),t.push({type:"fill",description:"Filled password field",selector:a.passwordRef,timestamp:Date.now()}),await this.browser.click(a.submitRef),t.push({type:"click",description:"Clicked login button",selector:a.submitRef,timestamp:Date.now()}),await this.browser.wait(2e3),e.totp_secret){this.generateTOTP(e.totp_secret);t.push({type:"fill",description:"Entered TOTP code",timestamp:Date.now()})}e.fixed_otp&&t.push({type:"fill",description:"Entered fixed OTP",timestamp:Date.now()})}}generateTOTP(e){Math.floor(Date.now()/1e3/30);return"000000"}async handleDebugWait(e,t){console.log("\n"+"=".repeat(60)),console.log("šŸ”“ DEBUG WAIT - Browser paused for manual interaction"),console.log("=".repeat(60)),console.log("\nYou can now interact with the browser manually."),console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n"),console.log("Press ENTER when done to capture cookies & localStorage...\n"),t.push({type:"info",description:"Paused for manual interaction (@debug_wait)",timestamp:Date.now()}),await new Promise(e=>{const t=()=>{process.stdin.removeListener("data",t),process.stdin.pause(),e()};process.stdin.resume(),process.stdin.once("data",t)}),console.log("\nšŸ“ø Capturing browser state...\n");const o=await this.browser.getCookies();let r=await this.browser.getLocalStorage(),n=await this.browser.getSessionStorage();const c=e=>{if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,a]of Object.entries(e))t[String(s)]="string"==typeof a?a:JSON.stringify(a);return t};r=c(r),n=c(n);const l={type:"inject"};o.length>0&&(l.cookies=o.map(e=>({name:e.name,value:e.value}))),Object.keys(r).length>0&&(l.localStorage=r),Object.keys(n).length>0&&(l.sessionStorage=n);const p=a.join(process.cwd(),"temp_credentials.yaml");let w={profiles:{}};if(s.existsSync(p))try{const e=i.parse(s.readFileSync(p,"utf-8"));e&&e.profiles&&"object"==typeof e.profiles&&(w={profiles:e.profiles})}catch{w={profiles:{}}}w.profiles[e]=l;const h=`# Captured credentials from @debug_wait\n# Generated: ${(new Date).toISOString()}\n#\n# To use: copy the profile you need to .slapify/credentials.yaml\n# Then use: @inject ${e}\n\n${i.stringify(w,{indent:2,lineWidth:0})}`;s.writeFileSync(p,h),console.log("āœ… Captured:"),console.log(` - ${o.length} cookie(s)`),console.log(` - ${Object.keys(r).length} localStorage item(s)`),console.log(` - ${Object.keys(n).length} sessionStorage item(s)`),console.log(`\nšŸ“ Saved to: ${p}`),console.log(` Profile name: "${e}"`),console.log("\n"+"=".repeat(60)+"\n"),t.push({type:"info",description:`Captured ${o.length} cookies, ${Object.keys(r).length} localStorage, ${Object.keys(n).length} sessionStorage to temp_credentials.yaml`,timestamp:Date.now()})}getActionType(e){switch(e){case"navigate":return"navigate";case"click":case"hover":case"press":return"click";case"fill":case"type":case"select":return"fill";case"wait":case"waitForText":return"wait";default:return"info"}}}
@@ -0,0 +1,5 @@
1
+ export { runTask } from "./runner.js";
2
+ export { listSessions, loadSession, loadEvents } from "./session.js";
3
+ export { saveTaskReport, generateTaskReportHtml } from "./report.js";
4
+ export type { TaskRunOptions, TaskSession, TaskEvent, TaskStatus, } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ export{runTask}from"./runner.js";export{listSessions,loadSession,loadEvents}from"./session.js";export{saveTaskReport,generateTaskReportHtml}from"./report.js";
@@ -0,0 +1,9 @@
1
+ import { TaskSession, SessionEvent } from "./types.js";
2
+ export interface TaskReport {
3
+ session: TaskSession;
4
+ events: SessionEvent[];
5
+ generatedAt: string;
6
+ }
7
+ export declare function generateTaskReportHtml(report: TaskReport): string;
8
+ export declare function saveTaskReport(session: TaskSession, events: SessionEvent[], outputDir?: string): string;
9
+ //# sourceMappingURL=report.d.ts.map