slapify 0.0.14 → 0.0.16

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 (50) hide show
  1. package/README.md +342 -258
  2. package/dist/ai/interpreter.d.ts +13 -0
  3. package/dist/ai/interpreter.d.ts.map +1 -1
  4. package/dist/ai/interpreter.js +43 -5
  5. package/dist/ai/interpreter.js.map +1 -1
  6. package/dist/cli.js +390 -152
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/perf/audit.d.ts +215 -0
  13. package/dist/perf/audit.d.ts.map +1 -0
  14. package/dist/perf/audit.js +635 -0
  15. package/dist/perf/audit.js.map +1 -0
  16. package/dist/report/generator.d.ts +1 -0
  17. package/dist/report/generator.d.ts.map +1 -1
  18. package/dist/report/generator.js +92 -0
  19. package/dist/report/generator.js.map +1 -1
  20. package/dist/runner/index.d.ts +14 -1
  21. package/dist/runner/index.d.ts.map +1 -1
  22. package/dist/runner/index.js +164 -4
  23. package/dist/runner/index.js.map +1 -1
  24. package/dist/task/index.d.ts +5 -0
  25. package/dist/task/index.d.ts.map +1 -0
  26. package/dist/task/index.js +4 -0
  27. package/dist/task/index.js.map +1 -0
  28. package/dist/task/report.d.ts +9 -0
  29. package/dist/task/report.d.ts.map +1 -0
  30. package/dist/task/report.js +740 -0
  31. package/dist/task/report.js.map +1 -0
  32. package/dist/task/runner.d.ts +3 -0
  33. package/dist/task/runner.d.ts.map +1 -0
  34. package/dist/task/runner.js +1362 -0
  35. package/dist/task/runner.js.map +1 -0
  36. package/dist/task/session.d.ts +18 -0
  37. package/dist/task/session.d.ts.map +1 -0
  38. package/dist/task/session.js +153 -0
  39. package/dist/task/session.js.map +1 -0
  40. package/dist/task/tools.d.ts +253 -0
  41. package/dist/task/tools.d.ts.map +1 -0
  42. package/dist/task/tools.js +258 -0
  43. package/dist/task/tools.js.map +1 -0
  44. package/dist/task/types.d.ts +153 -0
  45. package/dist/task/types.d.ts.map +1 -0
  46. package/dist/task/types.js +2 -0
  47. package/dist/task/types.js.map +1 -0
  48. package/dist/types.d.ts +2 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +20 -13
package/dist/cli.js CHANGED
@@ -427,6 +427,7 @@ program
427
427
  .option("--credentials <profile>", "Default credentials profile to use")
428
428
  .option("-p, --parallel", "Run tests in parallel")
429
429
  .option("-w, --workers <n>", "Number of parallel workers (default: 4)", "4")
430
+ .option("--performance", "Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report")
430
431
  .action(async (files, options) => {
431
432
  try {
432
433
  // Load configuration
@@ -574,12 +575,28 @@ program
574
575
  console.log(chalk.red(` └─ ${stepResult.error}`));
575
576
  }
576
577
  // Show assumptions if any
577
- if (stepResult.assumptions && stepResult.assumptions.length > 0) {
578
+ if (stepResult.assumptions &&
579
+ stepResult.assumptions.length > 0) {
578
580
  for (const assumption of stepResult.assumptions) {
579
581
  console.log(chalk.gray(` └─ 💡 ${assumption}`));
580
582
  }
581
583
  }
582
- });
584
+ }, !!options.performance);
585
+ // Show perf summary inline if audit was run
586
+ if (result.perfAudit) {
587
+ const p = result.perfAudit;
588
+ const parts = [];
589
+ if (p.vitals.fcp)
590
+ parts.push(`FCP ${p.vitals.fcp}ms`);
591
+ if (p.vitals.lcp)
592
+ parts.push(`LCP ${p.vitals.lcp}ms`);
593
+ if (p.vitals.cls != null)
594
+ parts.push(`CLS ${p.vitals.cls}`);
595
+ const s = p.scores ?? p.lighthouse;
596
+ if (s)
597
+ parts.push(`Perf ${s.performance}/100`);
598
+ console.log(chalk.cyan(` ⚡ Perf: ${parts.join(" · ")}`));
599
+ }
583
600
  results.push(result);
584
601
  // Print test summary
585
602
  console.log("");
@@ -693,9 +710,9 @@ program
693
710
  program
694
711
  .command("generate <prompt>")
695
712
  .alias("gen")
696
- .description("Generate a flow file from a natural language prompt")
713
+ .description("Generate a verified .flow file by running the goal as a task and recording what worked")
697
714
  .option("-d, --dir <directory>", "Directory to save flow", "tests")
698
- .option("--headed", "Show browser while analyzing")
715
+ .option("--headed", "Show browser window while running")
699
716
  .action(async (prompt, options) => {
700
717
  const configDir = getConfigDir();
701
718
  if (!configDir) {
@@ -703,156 +720,42 @@ program
703
720
  process.exit(1);
704
721
  }
705
722
  const config = loadConfig(configDir);
706
- const readline = await import("readline");
707
- console.log(chalk.blue("\n🤖 AI Flow Generator\n"));
708
- // Step 1: Extract URL from prompt using AI
709
- let spinner = ora("Analyzing prompt...").start();
710
- const urlResponse = await generateText({
711
- model: getModelFromConfig(config.llm),
712
- prompt: `Extract the URL from this test request. If there's no URL mentioned, respond with "NO_URL".
713
-
714
- Request: "${prompt}"
715
-
716
- Respond with ONLY the URL (e.g., https://example.com) or "NO_URL". Nothing else.`,
717
- maxTokens: 100,
718
- });
719
- let url = urlResponse.text.trim();
720
- spinner.stop();
721
- // Step 2: If no URL, ask user
722
- if (url === "NO_URL" || !url.startsWith("http")) {
723
- const rl = readline.createInterface({
724
- input: process.stdin,
725
- output: process.stdout,
726
- });
727
- url = await new Promise((resolve) => {
728
- rl.question(chalk.cyan("Enter the URL to test: "), (answer) => {
729
- rl.close();
730
- resolve(answer.trim());
731
- });
732
- });
733
- if (!url) {
734
- console.log(chalk.red("No URL provided. Aborting."));
735
- process.exit(1);
736
- }
737
- }
738
- console.log(chalk.gray(`URL: ${url}\n`));
739
- // Step 3: Open browser and get page snapshot
740
- spinner = ora("Opening browser and analyzing page...").start();
741
- const browser = new BrowserAgent({
742
- ...config.browser,
743
- headless: !options.headed,
723
+ console.log(chalk.blue("\n🤖 Flow Generator\n"));
724
+ console.log(chalk.gray(" Running the goal in the browser to discover the real path...\n"));
725
+ // Delegate to the task agent with save-flow enabled.
726
+ // The agent actually executes every step, handles login/captcha/popups,
727
+ // and writes only steps that are proven to work.
728
+ const { runTask } = await import("./task/runner.js");
729
+ let savedPath;
730
+ await runTask({
731
+ goal: prompt,
732
+ headed: options.headed,
733
+ saveFlow: true,
734
+ flowOutputDir: options.dir,
735
+ onEvent: (event) => {
736
+ if (event.type === "status_update") {
737
+ process.stdout.write(chalk.gray(` → ${event.message}\n`));
738
+ }
739
+ if (event.type === "message") {
740
+ console.log(chalk.white(`\n${event.text}`));
741
+ }
742
+ if (event.type === "flow_saved") {
743
+ savedPath = event.path;
744
+ }
745
+ if (event.type === "done") {
746
+ console.log(chalk.green(`\n✅ Done`));
747
+ }
748
+ if (event.type === "error") {
749
+ console.log(chalk.red(`\n✗ ${event.error}`));
750
+ }
751
+ },
744
752
  });
745
- try {
746
- await browser.navigate(url);
747
- await browser.wait(2000); // Wait for page to load
748
- const snapshot = await browser.snapshot(true);
749
- const pageTitle = await browser.getTitle();
750
- spinner.succeed("Page analyzed");
751
- // Step 4: Generate test steps using AI
752
- spinner = ora("Generating test steps...").start();
753
- const generationResponse = await generateText({
754
- model: getModelFromConfig(config.llm),
755
- system: `You are a test automation expert. Generate clear, actionable test steps for a .flow file.
756
-
757
- Rules:
758
- - Each step should be a single action or verification
759
- - Use natural language that describes WHAT to do, not HOW
760
- - Include [Optional] prefix for steps that might not always apply (popups, banners)
761
- - Include verification steps to confirm actions worked
762
- - Use "If X appears, do Y" for conditional handling
763
- - Keep steps concise but clear
764
- - Start with navigation to the URL
765
- - Generate a suitable filename (lowercase, hyphenated, no extension)
766
-
767
- Output format:
768
- FILENAME: suggested-name
769
- STEPS:
770
- Go to <url>
771
- Step 2 here
772
- Step 3 here
773
- ...`,
774
- prompt: `Generate test steps for this request:
775
-
776
- "${prompt}"
777
-
778
- Target URL: ${url}
779
- Page Title: ${pageTitle}
780
-
781
- Current page structure:
782
- ${snapshot}
783
-
784
- Generate practical test steps that accomplish the user's goal.`,
785
- maxTokens: 1500,
786
- });
787
- spinner.succeed("Test steps generated");
788
- // Parse the response
789
- const responseText = generationResponse.text;
790
- const filenameMatch = responseText.match(/FILENAME:\s*(.+)/i);
791
- const stepsMatch = responseText.match(/STEPS:\s*([\s\S]+)/i);
792
- let filename = filenameMatch
793
- ? filenameMatch[1]
794
- .trim()
795
- .replace(/[^a-z0-9-]/gi, "-")
796
- .toLowerCase()
797
- : "generated-test";
798
- if (!filename.endsWith(".flow")) {
799
- filename += ".flow";
800
- }
801
- const steps = stepsMatch
802
- ? stepsMatch[1].trim()
803
- : responseText.replace(/FILENAME:.+/i, "").trim();
804
- // Close browser
805
- await browser.close();
806
- // Step 5: Show generated steps and confirm
807
- console.log(chalk.blue("\n━━━ Generated Flow ━━━\n"));
808
- console.log(chalk.white(steps));
809
- console.log(chalk.blue("\n━━━━━━━━━━━━━━━━━━━━━━\n"));
810
- const rl = readline.createInterface({
811
- input: process.stdin,
812
- output: process.stdout,
813
- });
814
- const confirm = await new Promise((resolve) => {
815
- rl.question(chalk.cyan(`Save as ${options.dir}/${filename}? (Y/n/edit): `), (answer) => {
816
- rl.close();
817
- resolve(answer.trim().toLowerCase());
818
- });
819
- });
820
- if (confirm === "n" || confirm === "no") {
821
- console.log(chalk.yellow("Cancelled."));
822
- return;
823
- }
824
- if (confirm === "e" || confirm === "edit") {
825
- // Let user edit the filename
826
- const rl2 = readline.createInterface({
827
- input: process.stdin,
828
- output: process.stdout,
829
- });
830
- filename = await new Promise((resolve) => {
831
- rl2.question(chalk.cyan("Filename: "), (answer) => {
832
- rl2.close();
833
- const name = answer.trim() || filename;
834
- resolve(name.endsWith(".flow") ? name : name + ".flow");
835
- });
836
- });
837
- }
838
- // Step 6: Save the file
839
- const dir = options.dir;
840
- if (!fs.existsSync(dir)) {
841
- fs.mkdirSync(dir, { recursive: true });
842
- }
843
- const filepath = path.join(dir, filename);
844
- const content = `# ${filename
845
- .replace(".flow", "")
846
- .replace(/-/g, " ")}\n# Generated from: ${prompt}\n\n${steps}\n`;
847
- fs.writeFileSync(filepath, content);
848
- console.log(chalk.green(`\n✓ Saved: ${filepath}`));
849
- console.log(chalk.gray(`\nRun with: slapify run ${filepath}`));
753
+ if (savedPath) {
754
+ console.log(chalk.green(`\n✓ Flow saved: ${savedPath}`));
755
+ console.log(chalk.gray(` Run with: slapify run ${savedPath}`));
850
756
  }
851
- catch (error) {
852
- spinner.fail("Error");
853
- await browser.close();
854
- console.error(chalk.red(`Error: ${error.message}`));
855
- process.exit(1);
757
+ else {
758
+ console.log(chalk.yellow("\n⚠ No flow was saved. The agent may not have completed the goal."));
856
759
  }
857
760
  });
858
761
  // Fix command - analyze and fix failing tests
@@ -1312,5 +1215,340 @@ program
1312
1215
  };
1313
1216
  prompt();
1314
1217
  });
1218
+ // ─── Task command ─────────────────────────────────────────────────────────────
1219
+ program
1220
+ .command("task [goal]")
1221
+ .description("Run an autonomous AI agent task in plain English.\n" +
1222
+ " The agent decides everything: what to do, when to schedule, when to sleep.\n" +
1223
+ " Examples:\n" +
1224
+ ' slapify task "Go to linkedin.com and like the latest 3 posts"\n' +
1225
+ ' slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n' +
1226
+ ' slapify task "Order breakfast from Swiggy every day at 8am"')
1227
+ .option("--headed", "Show the browser window")
1228
+ .option("--debug", "Show all tool calls and internal steps")
1229
+ .option("--report", "Generate an HTML report after the task completes")
1230
+ .option("--save-flow", "Save agent steps as a reusable .flow file when done")
1231
+ .option("--session <id>", "Resume an existing task session")
1232
+ .option("--list-sessions", "List all task sessions")
1233
+ .option("--logs <id>", "Show logs for a task session")
1234
+ .option("--max-iterations <n>", "Safety cap on agent iterations (default 200)", parseInt)
1235
+ .action(async (goal, options) => {
1236
+ // Sub-command: list sessions
1237
+ if (options.listSessions) {
1238
+ const { listSessions } = await import("./task/index.js");
1239
+ const sessions = listSessions();
1240
+ if (sessions.length === 0) {
1241
+ console.log(chalk.gray("\nNo task sessions found.\n"));
1242
+ return;
1243
+ }
1244
+ console.log(chalk.blue(`\n📋 Task Sessions (${sessions.length})\n`));
1245
+ for (const s of sessions) {
1246
+ const statusColor = s.status === "completed"
1247
+ ? chalk.green
1248
+ : s.status === "failed"
1249
+ ? chalk.red
1250
+ : s.status === "scheduled"
1251
+ ? chalk.blue
1252
+ : chalk.yellow;
1253
+ console.log(` ${statusColor("●")} ${chalk.bold(s.id)}\n` +
1254
+ ` Goal: ${s.goal.slice(0, 70)}${s.goal.length > 70 ? "…" : ""}\n` +
1255
+ ` Status: ${statusColor(s.status)} Iterations: ${s.iteration}\n` +
1256
+ ` Updated: ${new Date(s.updatedAt).toLocaleString()}\n`);
1257
+ }
1258
+ return;
1259
+ }
1260
+ // Sub-command: show logs
1261
+ if (options.logs) {
1262
+ const { loadSession } = await import("./task/index.js");
1263
+ const { loadEvents } = await import("./task/session.js");
1264
+ const session = loadSession(options.logs);
1265
+ if (!session) {
1266
+ console.log(chalk.red(`Session '${options.logs}' not found.`));
1267
+ process.exit(1);
1268
+ }
1269
+ console.log(chalk.blue(`\n📜 Logs: ${session.id}\n`));
1270
+ console.log(chalk.gray(`Goal: ${session.goal}\n`));
1271
+ const events = loadEvents(options.logs);
1272
+ for (const event of events) {
1273
+ const ts = chalk.gray(new Date(event.ts).toLocaleTimeString());
1274
+ if (event.type === "llm_response") {
1275
+ if (event.text)
1276
+ console.log(`${ts} 🤔 ${chalk.cyan(event.text.slice(0, 120))}`);
1277
+ }
1278
+ else if (event.type === "tool_call") {
1279
+ console.log(`${ts} 🔧 ${chalk.yellow(event.toolName)} → ${chalk.gray(JSON.stringify(event.result).slice(0, 80))}`);
1280
+ }
1281
+ else if (event.type === "tool_error") {
1282
+ console.log(`${ts} ❌ ${chalk.red(event.toolName)} → ${chalk.red(event.error.slice(0, 80))}`);
1283
+ }
1284
+ else if (event.type === "memory_update") {
1285
+ console.log(`${ts} 🧠 ${chalk.magenta("remember")} ${event.key} = ${event.value.slice(0, 60)}`);
1286
+ }
1287
+ else if (event.type === "scheduled") {
1288
+ console.log(`${ts} ⏰ ${chalk.blue("schedule")} ${event.cron} — ${event.task}`);
1289
+ }
1290
+ else if (event.type === "sleeping_until") {
1291
+ console.log(`${ts} 😴 ${chalk.blue("sleep")} until ${event.until}`);
1292
+ }
1293
+ else if (event.type === "session_end") {
1294
+ console.log(`${ts} ✅ ${chalk.green("done")} ${event.summary.slice(0, 120)}`);
1295
+ }
1296
+ }
1297
+ console.log("");
1298
+ return;
1299
+ }
1300
+ // Determine goal
1301
+ let taskGoal = goal || options.session ? goal : undefined;
1302
+ // Resume without goal is fine — goal is stored in session
1303
+ if (!taskGoal && !options.session) {
1304
+ console.log(chalk.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n'));
1305
+ process.exit(1);
1306
+ }
1307
+ // If resuming, load the goal from session
1308
+ if (!taskGoal && options.session) {
1309
+ const { loadSession } = await import("./task/index.js");
1310
+ const s = loadSession(options.session);
1311
+ if (!s) {
1312
+ console.log(chalk.red(`Session '${options.session}' not found.`));
1313
+ process.exit(1);
1314
+ }
1315
+ taskGoal = s.goal;
1316
+ }
1317
+ // Check config
1318
+ const configDir = getConfigDir();
1319
+ if (!configDir) {
1320
+ console.log(chalk.red('\nNo .slapify directory found. Run "slapify init" first.\n'));
1321
+ process.exit(1);
1322
+ }
1323
+ // Track current session so SIGINT can generate report
1324
+ let activeSession = null;
1325
+ const generateAndPrintReport = async (session) => {
1326
+ if (!options.report)
1327
+ return;
1328
+ try {
1329
+ const { loadEvents, saveTaskReport } = await import("./task/index.js");
1330
+ const events = loadEvents(session.id);
1331
+ const reportPath = saveTaskReport(session, events);
1332
+ console.log(chalk.cyan(`\n 📊 Report: ${reportPath}`));
1333
+ }
1334
+ catch (e) {
1335
+ console.log(chalk.yellow(` ⚠ Could not generate report: ${e?.message}`));
1336
+ }
1337
+ };
1338
+ const debug = !!options.debug;
1339
+ // Clear the "thinking..." spinner line
1340
+ const clearLine = () => process.stdout.write("\x1b[2K\r");
1341
+ const printEvent = (event) => {
1342
+ switch (event.type) {
1343
+ // ── Debug-only (verbose internal steps) ───────────────────────────
1344
+ case "thinking":
1345
+ if (debug)
1346
+ process.stdout.write(chalk.gray(" ⟳ thinking...\r"));
1347
+ break;
1348
+ case "message":
1349
+ if (debug) {
1350
+ clearLine();
1351
+ console.log(chalk.gray(` 💬 ${event.text}`));
1352
+ }
1353
+ break;
1354
+ case "tool_start":
1355
+ if (debug) {
1356
+ clearLine();
1357
+ const argStr = JSON.stringify(event.args);
1358
+ console.log(chalk.dim(` › ${chalk.cyan(event.toolName)} `) +
1359
+ chalk.gray(argStr.slice(0, 100) + (argStr.length > 100 ? "…" : "")));
1360
+ }
1361
+ break;
1362
+ case "tool_done":
1363
+ if (debug) {
1364
+ console.log(chalk.dim(` ✓ ${event.result.slice(0, 120)}`));
1365
+ }
1366
+ break;
1367
+ case "tool_error":
1368
+ // Always show errors
1369
+ clearLine();
1370
+ console.log(chalk.red(` ✗ ${event.toolName}: ${event.error.slice(0, 120)}`));
1371
+ break;
1372
+ // ── Always visible ────────────────────────────────────────────────
1373
+ case "status_update":
1374
+ clearLine();
1375
+ console.log(chalk.white(` ${event.message}`));
1376
+ break;
1377
+ case "human_input_needed":
1378
+ // spinner is stopped by the trigger check above
1379
+ console.log("\n" + chalk.yellow("─".repeat(60)));
1380
+ console.log(chalk.yellow.bold(" 🙋 Agent needs your input"));
1381
+ console.log(chalk.white(`\n ${event.question}`));
1382
+ if (event.hint)
1383
+ console.log(chalk.gray(` ${event.hint}`));
1384
+ // Answer is handled via onHumanInput callback — just show the prompt here
1385
+ break;
1386
+ case "credentials_saved":
1387
+ clearLine();
1388
+ console.log(chalk.green(` 💾 Credentials saved: '${event.profileName}' (${event.credType}) → .slapify/credentials.yaml`));
1389
+ break;
1390
+ case "scheduled":
1391
+ if (debug) {
1392
+ clearLine();
1393
+ console.log(chalk.dim(` ⏰ scheduled: ${event.cron} — ${event.task}`));
1394
+ }
1395
+ break;
1396
+ case "sleeping":
1397
+ if (debug) {
1398
+ clearLine();
1399
+ console.log(chalk.dim(` 😴 sleeping until ${new Date(event.until).toLocaleString()}`));
1400
+ }
1401
+ break;
1402
+ case "done":
1403
+ clearLine();
1404
+ console.log("\n" + chalk.green("─".repeat(60)));
1405
+ console.log(chalk.green.bold(" ✅ Task complete!"));
1406
+ console.log(chalk.white(`\n ${event.summary}`));
1407
+ console.log(chalk.green("─".repeat(60)));
1408
+ break;
1409
+ case "error":
1410
+ clearLine();
1411
+ console.log(chalk.red(`\n ✗ Error: ${event.error}`));
1412
+ break;
1413
+ }
1414
+ };
1415
+ console.log(chalk.blue("\n🤖 Slapify Task Agent\n"));
1416
+ console.log(chalk.white(` Goal: ${taskGoal}`));
1417
+ if (options.session)
1418
+ console.log(chalk.gray(` Resuming session: ${options.session}`));
1419
+ console.log(chalk.gray([
1420
+ options.report ? " --report: HTML report on exit" : "",
1421
+ debug ? " --debug: verbose output" : "",
1422
+ " Ctrl+C to stop",
1423
+ ]
1424
+ .filter(Boolean)
1425
+ .join(" · ") + "\n"));
1426
+ console.log(chalk.gray("─".repeat(60)) + "\n");
1427
+ // Thinking spinner for default (non-debug) mode
1428
+ let spinnerInterval = null;
1429
+ let spinnerPaused = false; // true while waiting for human input
1430
+ const startSpinner = () => {
1431
+ if (debug || spinnerPaused || spinnerInterval)
1432
+ return;
1433
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
1434
+ let fi = 0;
1435
+ spinnerInterval = setInterval(() => {
1436
+ process.stdout.write(chalk.gray(`\r ${frames[fi++ % frames.length]} working...`));
1437
+ }, 80);
1438
+ };
1439
+ if (!debug)
1440
+ startSpinner();
1441
+ const { runTask } = await import("./task/index.js");
1442
+ // SIGINT handler — generate report then exit gracefully
1443
+ let sigintHandled = false;
1444
+ const onSigint = async () => {
1445
+ if (sigintHandled)
1446
+ return;
1447
+ sigintHandled = true;
1448
+ clearInterval(spinnerInterval);
1449
+ spinnerInterval = null;
1450
+ spinnerPaused = true;
1451
+ process.stdout.write("\x1b[2K\r");
1452
+ console.log(chalk.yellow("\n ⚡ Interrupted" +
1453
+ (options.report ? " — generating report..." : "")));
1454
+ if (activeSession) {
1455
+ activeSession.status = "failed";
1456
+ activeSession.finalSummary = "Task interrupted by user (Ctrl+C).";
1457
+ const { saveSessionMeta } = await import("./task/session.js");
1458
+ saveSessionMeta(activeSession);
1459
+ await generateAndPrintReport(activeSession);
1460
+ }
1461
+ console.log(chalk.gray(" Goodbye.\n"));
1462
+ process.exit(0);
1463
+ };
1464
+ process.once("SIGINT", onSigint);
1465
+ try {
1466
+ const stopSpinner = () => {
1467
+ if (spinnerInterval) {
1468
+ clearInterval(spinnerInterval);
1469
+ spinnerInterval = null;
1470
+ }
1471
+ process.stdout.write("\x1b[2K\r");
1472
+ };
1473
+ const session = await runTask({
1474
+ goal: taskGoal,
1475
+ sessionId: options.session,
1476
+ headed: options.headed,
1477
+ saveFlow: options.saveFlow,
1478
+ maxIterations: options.maxIterations,
1479
+ onHumanInput: async (question, hint) => {
1480
+ // Spinner is already stopped; block it from restarting while we read input
1481
+ spinnerPaused = true;
1482
+ stopSpinner();
1483
+ // Read a full line from stdin cleanly
1484
+ const readline = await import("readline");
1485
+ const rl = readline.createInterface({
1486
+ input: process.stdin,
1487
+ output: process.stdout,
1488
+ terminal: true,
1489
+ });
1490
+ const answer = await new Promise((resolve) => {
1491
+ rl.question(` ${chalk.cyan("›")} `, (ans) => {
1492
+ rl.close();
1493
+ resolve(ans.trim());
1494
+ });
1495
+ });
1496
+ console.log(chalk.yellow("─".repeat(60)) + "\n");
1497
+ // Unblock and restart spinner
1498
+ spinnerPaused = false;
1499
+ startSpinner();
1500
+ return answer;
1501
+ },
1502
+ onEvent: (event) => {
1503
+ const isVisible = event.type === "status_update" ||
1504
+ event.type === "human_input_needed" ||
1505
+ event.type === "credentials_saved" ||
1506
+ event.type === "done" ||
1507
+ event.type === "error" ||
1508
+ event.type === "tool_error";
1509
+ if (isVisible)
1510
+ stopSpinner();
1511
+ printEvent(event);
1512
+ // Restart spinner after visible output (but not if waiting for input or finished)
1513
+ if (isVisible &&
1514
+ event.type !== "done" &&
1515
+ event.type !== "error" &&
1516
+ event.type !== "human_input_needed" // onHumanInput restarts it after input
1517
+ ) {
1518
+ startSpinner();
1519
+ }
1520
+ },
1521
+ onSessionUpdate: (s) => {
1522
+ activeSession = s;
1523
+ },
1524
+ });
1525
+ stopSpinner();
1526
+ process.removeListener("SIGINT", onSigint);
1527
+ console.log(chalk.gray(`\n Session: ${session.id}`));
1528
+ if (session.savedFlowPath) {
1529
+ console.log(chalk.cyan(` Flow saved: ${session.savedFlowPath}`));
1530
+ }
1531
+ if (Object.keys(session.memory).length > 0) {
1532
+ console.log(chalk.gray(` Memory (${Object.keys(session.memory).length} items):`));
1533
+ for (const [k, v] of Object.entries(session.memory)) {
1534
+ console.log(chalk.gray(` • ${k}: ${v.slice(0, 80)}`));
1535
+ }
1536
+ }
1537
+ // Always generate report after task completes
1538
+ await generateAndPrintReport(session);
1539
+ console.log("");
1540
+ }
1541
+ catch (err) {
1542
+ process.removeListener("SIGINT", onSigint);
1543
+ clearInterval(spinnerInterval);
1544
+ spinnerInterval = null;
1545
+ process.stdout.write("\x1b[2K\r");
1546
+ console.error(chalk.red(`\n Task failed: ${err?.message || err}`));
1547
+ if (activeSession) {
1548
+ await generateAndPrintReport(activeSession);
1549
+ }
1550
+ process.exit(1);
1551
+ }
1552
+ });
1315
1553
  program.parse();
1316
1554
  //# sourceMappingURL=cli.js.map