cbrowser 5.2.0 → 6.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.
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ const mcp_server_js_1 = require("./mcp-server.js");
13
13
  function showHelp() {
14
14
  console.log(`
15
15
  ╔══════════════════════════════════════════════════════════════════════════════╗
16
- ║ CBrowser CLI v5.0.0 ║
17
- ║ AI-powered browser automation with smart retry & assertions
16
+ ║ CBrowser CLI v6.0.0 ║
17
+ ║ AI-powered browser automation with multi-persona comparison
18
18
  ╚══════════════════════════════════════════════════════════════════════════════╝
19
19
 
20
20
  NAVIGATION
@@ -36,8 +36,31 @@ AUTONOMOUS JOURNEYS
36
36
  --goal <goal> What to accomplish
37
37
  --record-video Record journey as video
38
38
 
39
+ MULTI-PERSONA COMPARISON (v6.0.0)
40
+ compare-personas Compare multiple personas on the same journey
41
+ --start <url> Starting URL (required)
42
+ --goal <goal> What to accomplish (required)
43
+ --personas <list> Comma-separated persona names
44
+ --concurrency <n> Max parallel browsers (default: 3)
45
+ --output <file> Save JSON report to file
46
+ --html Generate HTML report
47
+ Examples:
48
+ cbrowser compare-personas --start "https://example.com" \\
49
+ --goal "Complete checkout" \\
50
+ --personas power-user,first-timer,elderly-user,mobile-user
51
+
39
52
  PERSONAS
40
- persona list List available personas
53
+ persona list List all personas (built-in + custom)
54
+ persona create "<desc>" Create persona from natural language description
55
+ --name <name> Persona name (default: generated from description)
56
+ Examples:
57
+ cbrowser persona create "impatient developer who hates slow UIs"
58
+ cbrowser persona create "elderly user new to computers" --name grandma
59
+ cbrowser persona create "distracted teen on mobile phone"
60
+ persona show <name> Show detailed persona configuration
61
+ persona delete <name> Delete a custom persona
62
+ persona export <name> Export persona to JSON file
63
+ persona import <file> Import persona from JSON file
41
64
 
42
65
  SESSION MANAGEMENT
43
66
  session save <name> Save browser session (cookies, storage, URL)
@@ -276,6 +299,138 @@ function formatBytes(bytes) {
276
299
  return `${(bytes / 1024).toFixed(1)} KB`;
277
300
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
278
301
  }
302
+ function generateHtmlReport(comparison) {
303
+ const rows = comparison.personas.map((p) => `
304
+ <tr class="${p.success ? 'success' : 'failure'}">
305
+ <td><strong>${p.persona}</strong><br><small>${p.description}</small></td>
306
+ <td>${p.success ? '✓' : '✗'}</td>
307
+ <td>${(p.totalTime / 1000).toFixed(1)}s</td>
308
+ <td>${p.stepCount}</td>
309
+ <td>${p.frictionCount}</td>
310
+ <td>${p.techLevel}</td>
311
+ <td>${p.device}</td>
312
+ <td><small>${p.frictionPoints.join('<br>') || '-'}</small></td>
313
+ </tr>
314
+ `).join('');
315
+ return `<!DOCTYPE html>
316
+ <html lang="en">
317
+ <head>
318
+ <meta charset="UTF-8">
319
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
320
+ <title>Persona Comparison Report</title>
321
+ <style>
322
+ * { box-sizing: border-box; }
323
+ body {
324
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
325
+ max-width: 1400px;
326
+ margin: 0 auto;
327
+ padding: 2rem;
328
+ background: #f5f5f5;
329
+ }
330
+ h1 { color: #1a1a1a; border-bottom: 3px solid #3b82f6; padding-bottom: 0.5rem; }
331
+ .meta { background: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
332
+ .meta p { margin: 0.25rem 0; }
333
+ table {
334
+ width: 100%;
335
+ border-collapse: collapse;
336
+ background: white;
337
+ border-radius: 8px;
338
+ overflow: hidden;
339
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
340
+ }
341
+ th, td {
342
+ padding: 1rem;
343
+ text-align: left;
344
+ border-bottom: 1px solid #eee;
345
+ }
346
+ th { background: #1a1a1a; color: white; }
347
+ tr.success { background: #ecfdf5; }
348
+ tr.failure { background: #fef2f2; }
349
+ .summary {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
352
+ gap: 1rem;
353
+ margin: 1rem 0;
354
+ }
355
+ .stat {
356
+ background: white;
357
+ padding: 1rem;
358
+ border-radius: 8px;
359
+ text-align: center;
360
+ }
361
+ .stat .value { font-size: 2rem; font-weight: bold; color: #3b82f6; }
362
+ .stat .label { color: #666; font-size: 0.875rem; }
363
+ .recommendations {
364
+ background: #fffbeb;
365
+ border: 1px solid #fbbf24;
366
+ border-radius: 8px;
367
+ padding: 1rem;
368
+ margin-top: 1rem;
369
+ }
370
+ .recommendations h3 { margin-top: 0; }
371
+ .recommendations ul { margin: 0; padding-left: 1.5rem; }
372
+ </style>
373
+ </head>
374
+ <body>
375
+ <h1>🎭 Multi-Persona Comparison Report</h1>
376
+
377
+ <div class="meta">
378
+ <p><strong>URL:</strong> ${comparison.url}</p>
379
+ <p><strong>Goal:</strong> ${comparison.goal}</p>
380
+ <p><strong>Timestamp:</strong> ${comparison.timestamp}</p>
381
+ <p><strong>Total Duration:</strong> ${(comparison.duration / 1000).toFixed(1)}s</p>
382
+ </div>
383
+
384
+ <div class="summary">
385
+ <div class="stat">
386
+ <div class="value">${comparison.summary.successCount}/${comparison.summary.totalPersonas}</div>
387
+ <div class="label">Success Rate</div>
388
+ </div>
389
+ <div class="stat">
390
+ <div class="value">${(comparison.summary.avgCompletionTime / 1000).toFixed(1)}s</div>
391
+ <div class="label">Avg Completion Time</div>
392
+ </div>
393
+ <div class="stat">
394
+ <div class="value">${comparison.summary.fastestPersona}</div>
395
+ <div class="label">Fastest</div>
396
+ </div>
397
+ <div class="stat">
398
+ <div class="value">${comparison.summary.mostFriction}</div>
399
+ <div class="label">Most Friction</div>
400
+ </div>
401
+ </div>
402
+
403
+ <table>
404
+ <thead>
405
+ <tr>
406
+ <th>Persona</th>
407
+ <th>Success</th>
408
+ <th>Time</th>
409
+ <th>Steps</th>
410
+ <th>Friction</th>
411
+ <th>Tech Level</th>
412
+ <th>Device</th>
413
+ <th>Issues</th>
414
+ </tr>
415
+ </thead>
416
+ <tbody>
417
+ ${rows}
418
+ </tbody>
419
+ </table>
420
+
421
+ <div class="recommendations">
422
+ <h3>💡 Recommendations</h3>
423
+ <ul>
424
+ ${comparison.recommendations.map((r) => `<li>${r}</li>`).join('')}
425
+ </ul>
426
+ </div>
427
+
428
+ <p style="color: #999; text-align: center; margin-top: 2rem;">
429
+ Generated by CBrowser v6.0.0 - Multi-Persona Comparison
430
+ </p>
431
+ </body>
432
+ </html>`;
433
+ }
279
434
  function parseGeoLocation(location) {
280
435
  // Check if it's a preset
281
436
  if (types_js_1.LOCATION_PRESETS[location]) {
@@ -671,19 +826,255 @@ async function main() {
671
826
  }
672
827
  break;
673
828
  }
829
+ // =========================================================================
830
+ // Tier 6: Multi-Persona Comparison (v6.0.0)
831
+ // =========================================================================
832
+ case "compare-personas": {
833
+ const startUrl = options.start;
834
+ const goal = options.goal;
835
+ const personaList = options.personas;
836
+ if (!startUrl) {
837
+ console.error("Error: --start URL required");
838
+ process.exit(1);
839
+ }
840
+ if (!goal) {
841
+ console.error("Error: --goal required");
842
+ process.exit(1);
843
+ }
844
+ // Default to comparing all built-in personas if none specified
845
+ const personaNames = personaList
846
+ ? personaList.split(",").map((p) => p.trim())
847
+ : Object.keys(personas_js_1.BUILTIN_PERSONAS);
848
+ const concurrency = options.concurrency
849
+ ? parseInt(options.concurrency)
850
+ : 3;
851
+ const comparison = await (0, browser_js_1.comparePersonas)({
852
+ startUrl,
853
+ goal,
854
+ personas: personaNames,
855
+ maxConcurrency: concurrency,
856
+ headless,
857
+ });
858
+ // Print formatted report
859
+ const report = (0, browser_js_1.formatComparisonReport)(comparison);
860
+ console.log(report);
861
+ // Save JSON output if requested
862
+ if (options.output) {
863
+ const fs = await import("fs");
864
+ fs.writeFileSync(options.output, JSON.stringify(comparison, null, 2));
865
+ console.log(`\n📄 JSON report saved: ${options.output}`);
866
+ }
867
+ // Generate HTML report if requested
868
+ if (options.html) {
869
+ const fs = await import("fs");
870
+ const htmlReport = generateHtmlReport(comparison);
871
+ const htmlPath = options.output?.replace(".json", ".html") || "comparison-report.html";
872
+ fs.writeFileSync(htmlPath, htmlReport);
873
+ console.log(`\n🌐 HTML report saved: ${htmlPath}`);
874
+ }
875
+ // Exit with error if any personas failed
876
+ if (comparison.summary.failureCount > 0) {
877
+ process.exit(1);
878
+ }
879
+ break;
880
+ }
674
881
  case "persona": {
675
882
  const subcommand = args[0];
676
- if (subcommand === "list") {
677
- console.log("\n📋 Available Personas:\n");
678
- for (const [name, persona] of Object.entries(personas_js_1.BUILTIN_PERSONAS)) {
679
- console.log(` ${name}`);
680
- console.log(` ${persona.description}`);
681
- console.log(` Tech level: ${persona.demographics.tech_level}`);
682
- console.log("");
883
+ switch (subcommand) {
884
+ case "list": {
885
+ console.log("\n📋 Available Personas:\n");
886
+ // Built-in personas
887
+ console.log(" Built-in:");
888
+ for (const [name, persona] of Object.entries(personas_js_1.BUILTIN_PERSONAS)) {
889
+ console.log(` ${name}`);
890
+ console.log(` ${persona.description}`);
891
+ console.log(` Tech: ${persona.demographics.tech_level} | Device: ${persona.demographics.device}`);
892
+ console.log("");
893
+ }
894
+ // Custom personas
895
+ const customPersonas = (0, personas_js_1.loadCustomPersonas)();
896
+ const customNames = Object.keys(customPersonas);
897
+ if (customNames.length > 0) {
898
+ console.log(" Custom:");
899
+ for (const [name, persona] of Object.entries(customPersonas)) {
900
+ console.log(` ${name}`);
901
+ console.log(` ${persona.description}`);
902
+ console.log(` Tech: ${persona.demographics.tech_level} | Device: ${persona.demographics.device}`);
903
+ console.log("");
904
+ }
905
+ }
906
+ break;
683
907
  }
684
- }
685
- else {
686
- console.error("Usage: cbrowser persona list");
908
+ case "create": {
909
+ const description = args.slice(1).join(" ");
910
+ if (!description) {
911
+ console.error("Usage: cbrowser persona create \"<description>\" [--name <name>]");
912
+ console.error("\nExamples:");
913
+ console.error(" cbrowser persona create \"impatient developer who hates slow UIs\"");
914
+ console.error(" cbrowser persona create \"elderly user new to computers\" --name grandma");
915
+ console.error(" cbrowser persona create \"distracted teen on their phone\"");
916
+ process.exit(1);
917
+ }
918
+ // Generate name from description if not provided
919
+ let personaName = options.name;
920
+ if (!personaName) {
921
+ // Create a slug from first few words
922
+ personaName = description
923
+ .toLowerCase()
924
+ .replace(/[^a-z0-9\s]/g, "")
925
+ .split(/\s+/)
926
+ .slice(0, 3)
927
+ .join("-");
928
+ }
929
+ // Check if trying to overwrite built-in
930
+ if ((0, personas_js_1.isBuiltinPersona)(personaName)) {
931
+ console.error(`Error: Cannot overwrite built-in persona "${personaName}"`);
932
+ console.error("Use a different name with --name <name>");
933
+ process.exit(1);
934
+ }
935
+ console.log(`\n🤖 Generating persona from description...\n`);
936
+ console.log(` "${description}"\n`);
937
+ const persona = (0, personas_js_1.generatePersonaFromDescription)(personaName, description);
938
+ // Display the generated persona
939
+ console.log(`━━━ Generated Persona: ${persona.name} ━━━\n`);
940
+ console.log(`Description: ${persona.description}`);
941
+ console.log(`\nDemographics:`);
942
+ console.log(` Age Range: ${persona.demographics.age_range}`);
943
+ console.log(` Tech Level: ${persona.demographics.tech_level}`);
944
+ console.log(` Device: ${persona.demographics.device}`);
945
+ console.log(`\nTiming:`);
946
+ console.log(` Reaction Time: ${persona.humanBehavior?.timing.reactionTime.min}-${persona.humanBehavior?.timing.reactionTime.max}ms`);
947
+ console.log(` Click Delay: ${persona.humanBehavior?.timing.clickDelay.min}-${persona.humanBehavior?.timing.clickDelay.max}ms`);
948
+ console.log(` Type Speed: ${persona.humanBehavior?.timing.typeSpeed.min}-${persona.humanBehavior?.timing.typeSpeed.max}ms/char`);
949
+ console.log(` Reading Speed: ${persona.humanBehavior?.timing.readingSpeed} wpm`);
950
+ console.log(`\nError Rates:`);
951
+ console.log(` Misclick: ${((persona.humanBehavior?.errors.misClickRate || 0) * 100).toFixed(0)}%`);
952
+ console.log(` Accidental Double-click: ${((persona.humanBehavior?.errors.doubleClickAccidental || 0) * 100).toFixed(0)}%`);
953
+ console.log(` Typo: ${((persona.humanBehavior?.errors.typoRate || 0) * 100).toFixed(0)}%`);
954
+ console.log(`\nMouse Behavior:`);
955
+ console.log(` Speed: ${persona.humanBehavior?.mouse.speed}`);
956
+ console.log(` Curvature: ${persona.humanBehavior?.mouse.curvature}`);
957
+ console.log(` Jitter: ${persona.humanBehavior?.mouse.jitter}px`);
958
+ console.log(`\nAttention:`);
959
+ console.log(` Pattern: ${persona.humanBehavior?.attention.pattern}`);
960
+ console.log(` Scroll: ${persona.humanBehavior?.attention.scrollBehavior}`);
961
+ console.log(` Focus: ${persona.humanBehavior?.attention.focusAreas.join(", ")}`);
962
+ console.log(` Distraction Rate: ${((persona.humanBehavior?.attention.distractionRate || 0) * 100).toFixed(0)}%`);
963
+ console.log(`\nViewport: ${persona.context?.viewport?.[0]}x${persona.context?.viewport?.[1]}`);
964
+ if (Object.keys(persona.behaviors).length > 0) {
965
+ console.log(`\nBehaviors: ${Object.keys(persona.behaviors).join(", ")}`);
966
+ }
967
+ // Save the persona
968
+ const filepath = (0, personas_js_1.saveCustomPersona)(persona);
969
+ console.log(`\n✓ Persona saved: ${filepath}`);
970
+ console.log(`\nUse with: cbrowser journey ${personaName} --start <url> --goal "<goal>"`);
971
+ break;
972
+ }
973
+ case "show": {
974
+ const name = args[1];
975
+ if (!name) {
976
+ console.error("Usage: cbrowser persona show <name>");
977
+ process.exit(1);
978
+ }
979
+ // Check built-in first, then custom
980
+ let persona = personas_js_1.BUILTIN_PERSONAS[name];
981
+ let isCustom = false;
982
+ if (!persona) {
983
+ const customPersonas = (0, personas_js_1.loadCustomPersonas)();
984
+ persona = customPersonas[name];
985
+ isCustom = true;
986
+ }
987
+ if (!persona) {
988
+ console.error(`Persona not found: ${name}`);
989
+ console.error("Run 'cbrowser persona list' to see available personas");
990
+ process.exit(1);
991
+ }
992
+ console.log(`\n━━━ Persona: ${persona.name} ${isCustom ? "(custom)" : "(built-in)"} ━━━\n`);
993
+ console.log(JSON.stringify(persona, null, 2));
994
+ break;
995
+ }
996
+ case "delete": {
997
+ const name = args[1];
998
+ if (!name) {
999
+ console.error("Usage: cbrowser persona delete <name>");
1000
+ process.exit(1);
1001
+ }
1002
+ if ((0, personas_js_1.isBuiltinPersona)(name)) {
1003
+ console.error(`Cannot delete built-in persona: ${name}`);
1004
+ process.exit(1);
1005
+ }
1006
+ const deleted = (0, personas_js_1.deleteCustomPersona)(name);
1007
+ if (deleted) {
1008
+ console.log(`✓ Persona deleted: ${name}`);
1009
+ }
1010
+ else {
1011
+ console.error(`Custom persona not found: ${name}`);
1012
+ process.exit(1);
1013
+ }
1014
+ break;
1015
+ }
1016
+ case "export": {
1017
+ const name = args[1];
1018
+ if (!name) {
1019
+ console.error("Usage: cbrowser persona export <name>");
1020
+ process.exit(1);
1021
+ }
1022
+ // Get persona (built-in or custom)
1023
+ let persona = personas_js_1.BUILTIN_PERSONAS[name];
1024
+ if (!persona) {
1025
+ const customPersonas = (0, personas_js_1.loadCustomPersonas)();
1026
+ persona = customPersonas[name];
1027
+ }
1028
+ if (!persona) {
1029
+ console.error(`Persona not found: ${name}`);
1030
+ process.exit(1);
1031
+ }
1032
+ const fs = await import("fs");
1033
+ const filename = `${name}.persona.json`;
1034
+ fs.writeFileSync(filename, JSON.stringify(persona, null, 2));
1035
+ console.log(`✓ Exported to: ${filename}`);
1036
+ break;
1037
+ }
1038
+ case "import": {
1039
+ const file = args[1];
1040
+ if (!file) {
1041
+ console.error("Usage: cbrowser persona import <file>");
1042
+ process.exit(1);
1043
+ }
1044
+ const fs = await import("fs");
1045
+ if (!fs.existsSync(file)) {
1046
+ console.error(`File not found: ${file}`);
1047
+ process.exit(1);
1048
+ }
1049
+ try {
1050
+ const content = fs.readFileSync(file, "utf-8");
1051
+ const persona = JSON.parse(content);
1052
+ if (!persona.name || !persona.description) {
1053
+ console.error("Invalid persona file: missing name or description");
1054
+ process.exit(1);
1055
+ }
1056
+ if ((0, personas_js_1.isBuiltinPersona)(persona.name)) {
1057
+ console.error(`Cannot import: "${persona.name}" is a built-in persona name`);
1058
+ console.error("Edit the JSON file to change the name");
1059
+ process.exit(1);
1060
+ }
1061
+ const filepath = (0, personas_js_1.saveCustomPersona)(persona);
1062
+ console.log(`✓ Imported persona: ${persona.name}`);
1063
+ console.log(` Saved to: ${filepath}`);
1064
+ }
1065
+ catch (e) {
1066
+ console.error(`Failed to import: ${e.message}`);
1067
+ process.exit(1);
1068
+ }
1069
+ break;
1070
+ }
1071
+ default:
1072
+ console.error("Usage: cbrowser persona [list|create|show|delete|export|import]");
1073
+ console.error("\nExamples:");
1074
+ console.error(" cbrowser persona list");
1075
+ console.error(" cbrowser persona create \"impatient developer\" --name dev-persona");
1076
+ console.error(" cbrowser persona show power-user");
1077
+ console.error(" cbrowser persona delete my-custom-persona");
687
1078
  }
688
1079
  break;
689
1080
  }