teamshare-bridge 0.19.22 → 0.20.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 (91) hide show
  1. package/dist/cli/commands/browse.js +308 -59
  2. package/dist/cli/commands/browse.js.map +1 -1
  3. package/dist/cli/commands/build.js +31 -2
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/run.js +30 -0
  6. package/dist/cli/commands/run.js.map +1 -1
  7. package/dist/cli/commands/worker.js +33 -1
  8. package/dist/cli/commands/worker.js.map +1 -1
  9. package/dist/lib/api.d.ts +15 -0
  10. package/dist/lib/api.js +3 -0
  11. package/dist/lib/api.js.map +1 -1
  12. package/dist/lib/brief.js +14 -0
  13. package/dist/lib/brief.js.map +1 -1
  14. package/dist/lib/git/git-runner.d.ts +55 -0
  15. package/dist/lib/git/git-runner.js +323 -0
  16. package/dist/lib/git/git-runner.js.map +1 -0
  17. package/dist/lib/git/index.d.ts +2 -0
  18. package/dist/lib/git/index.js +5 -1
  19. package/dist/lib/git/index.js.map +1 -1
  20. package/dist/lib/llm/llm.js +10 -1
  21. package/dist/lib/llm/llm.js.map +1 -1
  22. package/dist/lib/run-report.d.ts +3 -0
  23. package/dist/lib/run-report.js +4 -0
  24. package/dist/lib/run-report.js.map +1 -1
  25. package/package.json +1 -1
  26. package/dist/bridge/register-only.d.ts +0 -2
  27. package/dist/bridge/register-only.js +0 -104
  28. package/dist/bridge/register-only.js.map +0 -1
  29. package/dist/lib/agy-pty.d.ts +0 -25
  30. package/dist/lib/agy-pty.js +0 -114
  31. package/dist/lib/agy-pty.js.map +0 -1
  32. package/dist/lib/chat-lock.d.ts +0 -8
  33. package/dist/lib/chat-lock.js +0 -59
  34. package/dist/lib/chat-lock.js.map +0 -1
  35. package/dist/lib/chat-reply.d.ts +0 -71
  36. package/dist/lib/chat-reply.js +0 -424
  37. package/dist/lib/chat-reply.js.map +0 -1
  38. package/dist/lib/daemon-reporter.d.ts +0 -17
  39. package/dist/lib/daemon-reporter.js +0 -125
  40. package/dist/lib/daemon-reporter.js.map +0 -1
  41. package/dist/lib/doc-reply.d.ts +0 -54
  42. package/dist/lib/doc-reply.js +0 -278
  43. package/dist/lib/doc-reply.js.map +0 -1
  44. package/dist/lib/git-auth.d.ts +0 -15
  45. package/dist/lib/git-auth.js +0 -92
  46. package/dist/lib/git-auth.js.map +0 -1
  47. package/dist/lib/git-auto.d.ts +0 -29
  48. package/dist/lib/git-auto.js +0 -141
  49. package/dist/lib/git-auto.js.map +0 -1
  50. package/dist/lib/git-pipeline.d.ts +0 -161
  51. package/dist/lib/git-pipeline.js +0 -598
  52. package/dist/lib/git-pipeline.js.map +0 -1
  53. package/dist/lib/harness-adapter.d.ts +0 -92
  54. package/dist/lib/harness-adapter.js +0 -735
  55. package/dist/lib/harness-adapter.js.map +0 -1
  56. package/dist/lib/harness-models.d.ts +0 -17
  57. package/dist/lib/harness-models.js +0 -150
  58. package/dist/lib/harness-models.js.map +0 -1
  59. package/dist/lib/harness-output.d.ts +0 -31
  60. package/dist/lib/harness-output.js +0 -270
  61. package/dist/lib/harness-output.js.map +0 -1
  62. package/dist/lib/harness.d.ts +0 -82
  63. package/dist/lib/harness.js +0 -362
  64. package/dist/lib/harness.js.map +0 -1
  65. package/dist/lib/llm.d.ts +0 -91
  66. package/dist/lib/llm.js +0 -831
  67. package/dist/lib/llm.js.map +0 -1
  68. package/dist/lib/models.d.ts +0 -51
  69. package/dist/lib/models.js +0 -124
  70. package/dist/lib/models.js.map +0 -1
  71. package/dist/lib/pr-create.d.ts +0 -30
  72. package/dist/lib/pr-create.js +0 -83
  73. package/dist/lib/pr-create.js.map +0 -1
  74. package/dist/lib/prompt-file.d.ts +0 -7
  75. package/dist/lib/prompt-file.js +0 -69
  76. package/dist/lib/prompt-file.js.map +0 -1
  77. package/dist/lib/retry.d.ts +0 -21
  78. package/dist/lib/retry.js +0 -60
  79. package/dist/lib/retry.js.map +0 -1
  80. package/dist/lib/session-stream.d.ts +0 -38
  81. package/dist/lib/session-stream.js +0 -204
  82. package/dist/lib/session-stream.js.map +0 -1
  83. package/dist/lib/slug.d.ts +0 -5
  84. package/dist/lib/slug.js +0 -18
  85. package/dist/lib/slug.js.map +0 -1
  86. package/dist/lib/usage-reader.d.ts +0 -46
  87. package/dist/lib/usage-reader.js +0 -206
  88. package/dist/lib/usage-reader.js.map +0 -1
  89. package/dist/lib/usage.d.ts +0 -18
  90. package/dist/lib/usage.js +0 -30
  91. package/dist/lib/usage.js.map +0 -1
@@ -299,7 +299,7 @@ async function cmdBrowse(flags) {
299
299
  const modelSpec = flags.get('model') ?? 'mimo:mimo-v2.5';
300
300
  const timeoutSec = Math.min(Math.max(parseInt(flags.get('timeout') ?? '300', 10), 30), 600);
301
301
  const timeoutMs = timeoutSec * 1000;
302
- const quality = Math.min(Math.max(parseInt(flags.get('quality') ?? '92', 10), 50), 100);
302
+ const quality = Math.min(Math.max(parseInt(flags.get('quality') ?? '78', 10), 50), 100);
303
303
  const maxConsecutiveFailures = parseInt(flags.get('max-consecutive-failures') ?? '0', 10); // 0 = unlimited
304
304
  const charDelayMs = Math.min(Math.max(parseInt(flags.get('char-delay') ?? '150', 10), 5), 200);
305
305
  const optimize = flags.has('optimize');
@@ -352,6 +352,117 @@ async function cmdBrowse(flags) {
352
352
  catch (err) {
353
353
  console.error(`[browse] Page.enable failed: ${(0, config_1.msg)(err)}`);
354
354
  }
355
+ // Stealth: install stack trace sanitization to hide CDP frames
356
+ installStackTraceStealth();
357
+ // Stealth: suppress console serialization leak caused by CDP's Runtime.enable.
358
+ // Anti-bot probe: Object.defineProperty(obj,'x',{get(){window.__detected=1}});
359
+ // console.debug(obj); if(window.__detected) → CDP is serializing args
360
+ // Countermeasure: wrap console methods to prevent deep serialization.
361
+ try {
362
+ await cdpEval(page, `
363
+ (() => {
364
+ const CK = Symbol('__ts_cp');
365
+ if (window[CK]) return;
366
+ const wrap = (fn) => function(...args) {
367
+ return fn.apply(console, args.map(a => {
368
+ if (a && typeof a === 'object' && typeof a !== 'function') {
369
+ try { return Object.assign(Array.isArray(a) ? [] : {}, a); } catch { return a; }
370
+ }
371
+ return a;
372
+ }));
373
+ };
374
+ console.debug = wrap(console.debug);
375
+ console.log = wrap(console.log);
376
+ console.warn = wrap(console.warn);
377
+ console.error = wrap(console.error);
378
+ window[CK] = true;
379
+ })()
380
+ `);
381
+ }
382
+ catch { /* non-critical */ }
383
+ // Register CDP event: Page.frameNavigated (URL/navigation changes)
384
+ onCdpEvent(page, 'Page.frameNavigated', () => {
385
+ page._cdpNavigated = Date.now();
386
+ });
387
+ // Inject MutationObserver: watches DOM for significant content changes
388
+ // Uses same fingerprint comparison as Node.js side to prevent false positives.
389
+ // Filters out: timers, animations, scripts, styles, notifications, toasts.
390
+ // Debounces mutations (500ms) to batch rapid changes.
391
+ // Stealth: uses Symbol-keyed property (invisible to Object.keys / anti-bot scans).
392
+ try {
393
+ await cdpEval(page, `
394
+ (() => {
395
+ const KEY = Symbol('__ts_obs');
396
+ if (window[KEY]) return 'already attached';
397
+ const IGNORE = /timer|countdown|elapsed|clock|animation|animate|transition|spinner|progress|loading|toast|notification|alert|banner|snackbar|cookie|consent|modal|overlay|popup|tooltip|badge|tag|avatar|icon|svg|img|video|audio|canvas|font|stylesheet|script|meta|link|head/i;
398
+ let timer = null;
399
+ let lastText = '';
400
+ function getText() {
401
+ const els = document.querySelectorAll('input[type="radio"],input[type="checkbox"],input[type="text"],input[type="email"],input[type="password"],textarea,label,button');
402
+ return [...els].map(e => {
403
+ const t = (e.innerText || e.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
404
+ return e.type + '|' + t + '|' + (e.checked || false);
405
+ }).join('\\n');
406
+ }
407
+ function check() {
408
+ try {
409
+ const curr = getText();
410
+ if (curr === lastText) return;
411
+ const prev = lastText;
412
+ lastText = curr;
413
+ const prevGroups = prev ? prev.split('\\n').filter(Boolean) : [];
414
+ const currGroups = curr.split('\\n').filter(Boolean);
415
+ const prevSet = new Set(prevGroups);
416
+ const currSet = new Set(currGroups);
417
+ let removed = 0, changed = 0;
418
+ for (const g of prevGroups) {
419
+ if (!currSet.has(g)) {
420
+ if (prevGroups.indexOf(g) < currGroups.length) changed++;
421
+ else removed++;
422
+ }
423
+ }
424
+ const added = currGroups.filter(g => !prevSet.has(g)).length;
425
+ const score = added * 10 + removed * 10 + changed * 8;
426
+ if (score >= 15) {
427
+ window[KEY].changed = Date.now();
428
+ }
429
+ } catch {}
430
+ }
431
+ const obs = new MutationObserver((mutations) => {
432
+ let hasRelevant = false;
433
+ for (const m of mutations) {
434
+ if (m.target instanceof Element) {
435
+ const tag = m.target.tagName;
436
+ if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'HEAD') continue;
437
+ const cls = m.target.className || '';
438
+ if (typeof cls === 'string' && IGNORE.test(cls)) continue;
439
+ const id = m.target.id || '';
440
+ if (IGNORE.test(id)) continue;
441
+ if (m.type === 'characterData' && m.target.parentElement) {
442
+ const ptag = m.target.parentElement.tagName;
443
+ if (ptag === 'SCRIPT' || ptag === 'STYLE') continue;
444
+ }
445
+ }
446
+ hasRelevant = true;
447
+ }
448
+ if (hasRelevant) {
449
+ clearTimeout(timer);
450
+ timer = setTimeout(check, 500);
451
+ }
452
+ });
453
+ obs.observe(document.body || document.documentElement, {
454
+ childList: true, subtree: true, characterData: true
455
+ });
456
+ window[KEY] = { observer: obs, changed: 0 };
457
+ lastText = getText();
458
+ return 'ok';
459
+ })()
460
+ `);
461
+ console.log('[browse] MutationObserver injected (stealth, debounced, filtered)');
462
+ }
463
+ catch (err) {
464
+ console.error(`[browse] MutationObserver injection failed: ${(0, config_1.msg)(err)}`);
465
+ }
355
466
  // Agent loop
356
467
  let actionCount = 0;
357
468
  let llmCallCount = 0;
@@ -381,10 +492,12 @@ async function cmdBrowse(flags) {
381
492
  continue;
382
493
  }
383
494
  // Detect screen change via question fingerprint comparison
495
+ // Threshold 15: requires ≥1 new/removed question (10) + text change (8) = 18,
496
+ // or 2 text changes (16). Score of 5 (single option change) is too sensitive.
384
497
  const currentFingerprint = createFingerprint(elements);
385
498
  if (lastFingerprint) {
386
499
  const changeScore = calculateChangeScore(lastFingerprint, currentFingerprint);
387
- if (changeScore >= 5) {
500
+ if (changeScore >= 15) {
388
501
  console.log(`[browse] Screen changed (score: ${changeScore}) — resetting state`);
389
502
  codeConfirmedOptimal = false;
390
503
  consecutiveFailures = 0;
@@ -398,7 +511,7 @@ async function cmdBrowse(flags) {
398
511
  console.log(`[browse] [${actionLabel}] Analyzing screenshot...`);
399
512
  let result = null;
400
513
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
401
- result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, attempt, maxRetries);
514
+ result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, attempt, maxRetries, page, lastFingerprint);
402
515
  llmCallCount++;
403
516
  if (llmCallCount > 50 && llmCallCount % 25 === 0) {
404
517
  console.log(`[browse] Warning: ${llmCallCount} LLM calls made — may hit rate limits`);
@@ -445,7 +558,7 @@ async function cmdBrowse(flags) {
445
558
  const freshFingerprint = createFingerprint(freshElements);
446
559
  if (lastFingerprint) {
447
560
  const changeScore = calculateChangeScore(lastFingerprint, freshFingerprint);
448
- if (changeScore >= 5) {
561
+ if (changeScore >= 15) {
449
562
  console.log(`[browse] Screen changed during LLM call (score: ${changeScore}) — skipping actions, re-analyzing`);
450
563
  lastFingerprint = freshFingerprint;
451
564
  lastScreenshot = freshScreenshot;
@@ -471,7 +584,7 @@ async function cmdBrowse(flags) {
471
584
  if (hasUnansweredQuestions(elements)) {
472
585
  console.log(`[browse] LLM says done but unanswered questions remain — retrying...`);
473
586
  // Force retry with stronger prompt
474
- result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, 1, 1);
587
+ result = await analyzeScreenshot(baseUrl, apiKey, model, screenshot, elements, pageContext, timeoutMs, 1, 1, page, lastFingerprint);
475
588
  if (result.actions.length > 0 && result.actions[0].action !== 'done') {
476
589
  // LLM returned actual actions — re-process from the start of this loop
477
590
  executedAny = false;
@@ -615,6 +728,10 @@ async function cmdBrowse(flags) {
615
728
  else {
616
729
  console.log(`[browse] Action: ${act.action} - ${act.reasoning || ''}`);
617
730
  }
731
+ // Human-like hesitation before acting (200-600ms)
732
+ if (act.action === 'click' || act.action === 'type' || act.action === 'write_code') {
733
+ await sleep(200 + Math.random() * 400);
734
+ }
618
735
  await (hybrid ? executeActionHybrid : executeAction)(page, act, elements, charDelayMs);
619
736
  actionCount++;
620
737
  executedAny = true;
@@ -646,7 +763,7 @@ Review the code and check:
646
763
  If the code is ALREADY optimal (top-tier efficiency), return: [{"action":"done","summary":"Code is already optimal"}]
647
764
 
648
765
  If you can optimize it, return the FULL optimized code using write_code. Focus ONLY on performance — do not change correctness.
649
- Do NOT simplify variable names or change the function signature. Only optimize the algorithm.`);
766
+ Do NOT simplify variable names or change the function signature. Only optimize the algorithm.`, page);
650
767
  if (optResult.actions.length > 0 && optResult.actions[0].action !== 'done') {
651
768
  const optAct = optResult.actions[0];
652
769
  if ((optAct.action === 'write_code' || optAct.action === 'type') && optAct.code) {
@@ -732,6 +849,7 @@ async function connectToPage(port) {
732
849
  title: pageTarget.title || pageTarget.url,
733
850
  msgId: 1,
734
851
  pending: new Map(),
852
+ eventListeners: new Map(),
735
853
  };
736
854
  const timeout = setTimeout(() => {
737
855
  ws.close();
@@ -748,6 +866,20 @@ async function connectToPage(port) {
748
866
  ws.on('message', (data) => {
749
867
  try {
750
868
  const msg = JSON.parse(data.toString());
869
+ // Handle CDP events (have method, no id)
870
+ if (msg.method && !msg.id) {
871
+ const listeners = page.eventListeners.get(msg.method);
872
+ if (listeners) {
873
+ for (const cb of listeners) {
874
+ try {
875
+ cb(msg.params || {});
876
+ }
877
+ catch { /* listener error */ }
878
+ }
879
+ }
880
+ return;
881
+ }
882
+ // Handle request/response (have id)
751
883
  if (msg.id && page.pending.has(msg.id)) {
752
884
  const p = page.pending.get(msg.id);
753
885
  page.pending.delete(msg.id);
@@ -772,6 +904,48 @@ async function connectToPage(port) {
772
904
  });
773
905
  });
774
906
  }
907
+ /** Register a listener for a CDP event (e.g. 'Page.frameNavigated'). */
908
+ function onCdpEvent(page, event, cb) {
909
+ if (!page.eventListeners.has(event))
910
+ page.eventListeners.set(event, new Set());
911
+ page.eventListeners.get(event).add(cb);
912
+ }
913
+ // ── Stealth: Stack trace sanitization ────────────────────────────────────────
914
+ // Anti-bot detection: try { throw new Error('probe') } catch(e) {
915
+ // if (e.stack.includes('Runtime.evaluate')) { /* DETECTED */ }
916
+ // }
917
+ // Countermeasure: override Error.prepareStackTrace to strip CDP frames,
918
+ // and add sourceURL to make injected scripts look like Chrome extensions.
919
+ const CDP_STACK_RE = /Runtime\.evaluate|at\s+.*?CDP|__cdp/i;
920
+ const STEALTH_SOURCE_URL = '//# sourceURL=chrome-extension://pbjikgmbmlelhpamhfbhjnknahkdbcm/inject.js';
921
+ /** Override Error.prepareStackTrace to strip CDP-related frames from stack traces. */
922
+ function installStackTraceStealth() {
923
+ if (typeof Error.prepareStackTrace === 'function')
924
+ return; // already installed
925
+ const origPrepare = Error.prepareStackTrace;
926
+ Error.prepareStackTrace = (error, structuredStackTrace) => {
927
+ // Filter out frames that contain CDP markers
928
+ const filtered = structuredStackTrace.filter((callSite) => {
929
+ const name = callSite.toString();
930
+ return !CDP_STACK_RE.test(name);
931
+ });
932
+ // If we filtered everything, fall back to original
933
+ if (filtered.length === 0) {
934
+ return origPrepare ? origPrepare(error, structuredStackTrace) : structuredStackTrace.map(s => s.toString()).join('\n');
935
+ }
936
+ return filtered.map(s => s.toString()).join('\n');
937
+ };
938
+ }
939
+ /**
940
+ * Stealth wrapper for Runtime.evaluate.
941
+ * Adds sourceURL marker (makes stack traces look like a Chrome extension)
942
+ * and sanitizes stack traces to hide CDP frames.
943
+ */
944
+ function cdpEval(page, expression, returnByValue = true) {
945
+ // Add sourceURL to disguise the injection origin
946
+ const wrapped = expression.trimEnd() + '\n' + STEALTH_SOURCE_URL;
947
+ return cdpSend(page, 'Runtime.evaluate', { expression: wrapped, returnByValue });
948
+ }
775
949
  function cdpSend(page, method, params) {
776
950
  return new Promise((resolve, reject) => {
777
951
  const id = page.msgId++;
@@ -955,10 +1129,7 @@ async function extractElements(page) {
955
1129
  })()
956
1130
  `;
957
1131
  try {
958
- const result = await cdpSend(page, 'Runtime.evaluate', {
959
- expression: js,
960
- returnByValue: true,
961
- });
1132
+ const result = await cdpEval(page, js);
962
1133
  return result?.result?.value || [];
963
1134
  }
964
1135
  catch {
@@ -988,12 +1159,15 @@ async function executeAction(page, act, elements, charDelayMs) {
988
1159
  console.log(`[browse] Scrolled — element now at index ${el.i}, position (${el.x},${el.y})`);
989
1160
  }
990
1161
  }
991
- console.log(`[browse] Clicking [${el.i}] ${el.tag} "${el.text.slice(0, 60)}" at (${el.x},${el.y})`);
1162
+ // Human-like click: offset ±2-5px from center (humans never click exact center)
1163
+ const clickX = el.x + Math.round((Math.random() - 0.5) * 8);
1164
+ const clickY = el.y + Math.round((Math.random() - 0.5) * 6);
1165
+ console.log(`[browse] Clicking [${el.i}] ${el.tag} "${el.text.slice(0, 60)}" at (${clickX},${clickY})`);
992
1166
  await cdpSend(page, 'Input.dispatchMouseEvent', {
993
- type: 'mousePressed', x: el.x, y: el.y, button: 'left', clickCount: 1,
1167
+ type: 'mousePressed', x: clickX, y: clickY, button: 'left', clickCount: 1,
994
1168
  });
995
1169
  await cdpSend(page, 'Input.dispatchMouseEvent', {
996
- type: 'mouseReleased', x: el.x, y: el.y, button: 'left', clickCount: 1,
1170
+ type: 'mouseReleased', x: clickX, y: clickY, button: 'left', clickCount: 1,
997
1171
  });
998
1172
  return true;
999
1173
  }
@@ -1113,8 +1287,10 @@ async function executeActionHybrid(page, act, elements, charDelayMs) {
1113
1287
  console.log(`[browse] Scrolled — element now at index ${el.i}, position (${el.x},${el.y})`);
1114
1288
  }
1115
1289
  }
1116
- console.log(`[browse] Clicking [${el.i}] ${el.tag} "${el.text.slice(0, 60)}" at (${el.x},${el.y})`);
1117
- clickOS(el.x, el.y);
1290
+ const clickX = el.x + Math.round((Math.random() - 0.5) * 8);
1291
+ const clickY = el.y + Math.round((Math.random() - 0.5) * 6);
1292
+ console.log(`[browse] Clicking [${el.i}] ${el.tag} "${el.text.slice(0, 60)}" at (${clickX},${clickY})`);
1293
+ clickOS(clickX, clickY);
1118
1294
  return true;
1119
1295
  }
1120
1296
  case 'type': {
@@ -1300,8 +1476,7 @@ async function clearEditor(page) {
1300
1476
  /** Read current editor content from Monaco/CM5/CM6/Ace. */
1301
1477
  async function readEditorContent(page) {
1302
1478
  try {
1303
- const result = await cdpSend(page, 'Runtime.evaluate', {
1304
- expression: `
1479
+ const result = await cdpEval(page, `
1305
1480
  (() => {
1306
1481
  const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1307
1482
  const editors = monacoNs?.editor?.getEditors?.() || [];
@@ -1314,9 +1489,7 @@ async function readEditorContent(page) {
1314
1489
  if (ace) return ace.getValue();
1315
1490
  return '';
1316
1491
  })()
1317
- `,
1318
- returnByValue: true,
1319
- });
1492
+ `);
1320
1493
  return result?.result?.value ?? '';
1321
1494
  }
1322
1495
  catch {
@@ -1326,8 +1499,7 @@ async function readEditorContent(page) {
1326
1499
  /** Scroll an off-screen element into view before clicking. */
1327
1500
  async function scrollElementIntoView(page, el) {
1328
1501
  const textSlice = el.text?.slice(0, 100) || '';
1329
- await cdpSend(page, 'Runtime.evaluate', {
1330
- expression: `
1502
+ await cdpEval(page, `
1331
1503
  (() => {
1332
1504
  const sels = [
1333
1505
  'input[type="radio"]', 'input[type="checkbox"]',
@@ -1347,9 +1519,7 @@ async function scrollElementIntoView(page, el) {
1347
1519
  }
1348
1520
  return false;
1349
1521
  })()
1350
- `,
1351
- returnByValue: true,
1352
- });
1522
+ `);
1353
1523
  await sleep(300);
1354
1524
  }
1355
1525
  /** Check if there are genuinely unanswered questions — catches premature 'done' from LLM. */
@@ -1515,19 +1685,36 @@ async function waitForScreenChange(page, previousScreenshot, intervalMs, quality
1515
1685
  }
1516
1686
  return false;
1517
1687
  }
1518
- /** Variable typing delay random variation for natural feel. Never faster than 70% of base. */
1688
+ /** Box-Muller transform: Gaussian-distributed random number (mean 0, stddev 1). */
1689
+ function gaussianRandom() {
1690
+ let u = 0, v = 0;
1691
+ while (u === 0)
1692
+ u = Math.random();
1693
+ while (v === 0)
1694
+ v = Math.random();
1695
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
1696
+ }
1697
+ /** Variable typing delay — Gaussian distribution with character-specific offsets. */
1519
1698
  function variableDelay(baseMs, char, index, total) {
1520
- let delay = baseMs + (Math.random() - 0.5) * baseMs * 0.6;
1699
+ // Gaussian delay centered on baseMs, stddev = 30% of base
1700
+ let delay = baseMs + gaussianRandom() * baseMs * 0.3;
1701
+ // Space bar: longer pause (word boundary)
1521
1702
  if (char === ' ')
1522
- delay += 20 + Math.random() * 60;
1703
+ delay += 30 + Math.random() * 80;
1704
+ // Digits: slight pause
1523
1705
  if (/[0-9]/.test(char))
1524
- delay += 10 + Math.random() * 30;
1706
+ delay += 15 + Math.random() * 40;
1707
+ // Special chars: longer pause (reaching for symbol)
1525
1708
  if (/^[{}():"<>!@#$%^&*_+|~`?\[\]]$/.test(char))
1526
- delay += 50 + Math.random() * 120;
1527
- if (index > 0 && index % (15 + Math.floor(Math.random() * 10)) === 0) {
1528
- delay += 200 + Math.random() * 500;
1529
- }
1530
- return Math.max(delay, baseMs * 0.7);
1709
+ delay += 60 + Math.random() * 150;
1710
+ // Occasional "thinking" pause (burst typing pattern)
1711
+ if (index > 0 && index % (12 + Math.floor(Math.random() * 8)) === 0) {
1712
+ delay += 300 + Math.random() * 700;
1713
+ }
1714
+ // Newline: longer pause (reading/thinking)
1715
+ if (char === '\n')
1716
+ delay += 80 + Math.random() * 200;
1717
+ return Math.max(delay, baseMs * 0.5);
1531
1718
  }
1532
1719
  /**
1533
1720
  * Detect and remove extra trailing closing brackets in the editor.
@@ -1593,8 +1780,7 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1593
1780
  });
1594
1781
  await sleep(100);
1595
1782
  // Disable ALL auto-formatting to prevent cascading indentation
1596
- await cdpSend(page, 'Runtime.evaluate', {
1597
- expression: `
1783
+ await cdpEval(page, `
1598
1784
  (() => {
1599
1785
  const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1600
1786
  monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
@@ -1613,8 +1799,7 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1613
1799
  ace.setOption(' behavioursEnabled', false);
1614
1800
  }
1615
1801
  })()
1616
- `,
1617
- });
1802
+ `);
1618
1803
  // Smart retry: check if editor already has the correct code
1619
1804
  const currentContent = await readEditorContent(page);
1620
1805
  if (currentContent === text) {
@@ -1675,8 +1860,7 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1675
1860
  // Post-typing: detect and remove extra trailing brackets
1676
1861
  await removeTrailingBrackets(page);
1677
1862
  // Re-enable auto-formatting
1678
- await cdpSend(page, 'Runtime.evaluate', {
1679
- expression: `
1863
+ await cdpEval(page, `
1680
1864
  (() => {
1681
1865
  const monacoNs = typeof monaco !== 'undefined' ? monaco : window.monaco;
1682
1866
  monacoNs?.editor?.getEditors?.().forEach(e => e.updateOptions({
@@ -1689,8 +1873,7 @@ async function insertIntoCodeEditor(page, selector, text, charDelayMs) {
1689
1873
  const cm5 = document.querySelector('.CodeMirror')?.CodeMirror;
1690
1874
  if (cm5) cm5.setOption('electricChars', true);
1691
1875
  })()
1692
- `,
1693
- });
1876
+ `);
1694
1877
  console.log(`[browse] Typing complete (${text.length} chars)`);
1695
1878
  }
1696
1879
  /** Insert text into a generic input — click focus + char-by-char typing. */
@@ -1831,10 +2014,7 @@ async function extractPageContext(page) {
1831
2014
  })()
1832
2015
  `;
1833
2016
  try {
1834
- const result = await cdpSend(page, 'Runtime.evaluate', {
1835
- expression: js,
1836
- returnByValue: true,
1837
- });
2017
+ const result = await cdpEval(page, js);
1838
2018
  return result?.result?.value ?? '';
1839
2019
  }
1840
2020
  catch {
@@ -1869,10 +2049,7 @@ async function detectEditor(page) {
1869
2049
  })()
1870
2050
  `;
1871
2051
  try {
1872
- const result = await cdpSend(page, 'Runtime.evaluate', {
1873
- expression: js,
1874
- returnByValue: true,
1875
- });
2052
+ const result = await cdpEval(page, js);
1876
2053
  return result?.result?.value ?? null;
1877
2054
  }
1878
2055
  catch {
@@ -1885,10 +2062,7 @@ async function detectEditor(page) {
1885
2062
  */
1886
2063
  async function focusSelector(page, selector) {
1887
2064
  try {
1888
- await cdpSend(page, 'Runtime.evaluate', {
1889
- expression: `document.querySelector('${selector}')?.focus()`,
1890
- returnByValue: true,
1891
- });
2065
+ await cdpEval(page, `document.querySelector('${selector}')?.focus()`);
1892
2066
  return true;
1893
2067
  }
1894
2068
  catch {
@@ -2057,7 +2231,7 @@ function extractScatteredActions(str) {
2057
2231
  }
2058
2232
  return actions;
2059
2233
  }
2060
- async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts) {
2234
+ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts, page, lastFingerprint) {
2061
2235
  const startTime = Date.now();
2062
2236
  // Format element list grouped by question
2063
2237
  const groups = new Map();
@@ -2131,11 +2305,62 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
2131
2305
  };
2132
2306
  const controller = new AbortController();
2133
2307
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
2134
- // Heartbeat: log progress every 30s while waiting
2135
- const heartbeat = setInterval(() => {
2308
+ // Heartbeat: check for screen changes every 5s, abort LLM if page changed
2309
+ const heartbeat = setInterval(async () => {
2136
2310
  const elapsed = Math.round((Date.now() - startTime) / 1000);
2311
+ // Check for screen change via CDP navigation event or MutationObserver flag
2312
+ let screenChanged = false;
2313
+ if (page?._cdpNavigated && page._cdpNavigated > startTime) {
2314
+ screenChanged = true;
2315
+ console.log(`[browse] CDP navigation detected (${elapsed}s) — aborting LLM`);
2316
+ }
2317
+ if (!screenChanged) {
2318
+ try {
2319
+ const flagResult = await cdpEval(page, `
2320
+ (() => {
2321
+ const syms = Object.getOwnPropertySymbols(window);
2322
+ for (const s of syms) {
2323
+ if (s.description === '__ts_obs') return window[s]?.changed || 0;
2324
+ }
2325
+ return 0;
2326
+ })()
2327
+ `);
2328
+ const flagTs = flagResult?.result?.value;
2329
+ if (flagTs && flagTs > startTime) {
2330
+ screenChanged = true;
2331
+ console.log(`[browse] DOM mutation detected (${elapsed}s) — aborting LLM`);
2332
+ // Reset flag
2333
+ await cdpEval(page, `
2334
+ (() => {
2335
+ const syms = Object.getOwnPropertySymbols(window);
2336
+ for (const s of syms) {
2337
+ if (s.description === '__ts_obs') { window[s].changed = 0; return; }
2338
+ }
2339
+ })()
2340
+ `);
2341
+ }
2342
+ }
2343
+ catch { /* page disconnected */ }
2344
+ }
2345
+ if (screenChanged && lastFingerprint && page) {
2346
+ // Verify with fingerprint comparison before aborting
2347
+ try {
2348
+ const freshElements = await extractElements(page);
2349
+ const freshFingerprint = createFingerprint(freshElements);
2350
+ const changeScore = calculateChangeScore(lastFingerprint, freshFingerprint);
2351
+ if (changeScore >= 15) {
2352
+ console.log(`[browse] Fingerprint confirms change (score: ${changeScore}) — aborting LLM (${elapsed}s)`);
2353
+ controller.abort();
2354
+ return;
2355
+ }
2356
+ else {
2357
+ console.log(`[browse] Fingerprint says no significant change (score: ${changeScore}) — ignoring (${elapsed}s)`);
2358
+ }
2359
+ }
2360
+ catch { /* extraction failed, skip */ }
2361
+ }
2137
2362
  console.log(`[browse] Waiting for LLM... (${elapsed}s elapsed, attempt ${attempt}/${maxAttempts})`);
2138
- }, 30000);
2363
+ }, 5000);
2139
2364
  const screenshotKB = Math.round((screenshotBase64.length * 3 / 4) / 1024);
2140
2365
  const contextKB = Math.round(pageContext.length / 1024);
2141
2366
  console.log(`[browse] LLM request: ${model} | ${screenshotKB}KB image | ${contextKB}KB context | ${elements.length} elements | timeout ${Math.round(timeoutMs / 1000)}s | attempt ${attempt}/${maxAttempts}`);
@@ -2195,7 +2420,7 @@ async function analyzeScreenshot(baseUrl, apiKey, model, screenshotBase64, eleme
2195
2420
  }
2196
2421
  }
2197
2422
  /** Like analyzeScreenshot but accepts a custom system prompt (for optimization pass). */
2198
- async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts, customSystemPrompt) {
2423
+ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBase64, elements, pageContext, timeoutMs, attempt, maxAttempts, customSystemPrompt, page) {
2199
2424
  const startTime = Date.now();
2200
2425
  const userText = [
2201
2426
  pageContext ? `<page_context>\n${pageContext}\n</page_context>\n` : '',
@@ -2219,6 +2444,29 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
2219
2444
  };
2220
2445
  const controller = new AbortController();
2221
2446
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
2447
+ // Heartbeat: check for screen changes every 5s, abort if page changed
2448
+ const heartbeat = setInterval(async () => {
2449
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
2450
+ try {
2451
+ const flagResult = await cdpEval(page, `
2452
+ (() => {
2453
+ const syms = Object.getOwnPropertySymbols(window);
2454
+ for (const s of syms) {
2455
+ if (s.description === '__ts_obs') return window[s]?.changed || 0;
2456
+ }
2457
+ return 0;
2458
+ })()
2459
+ `);
2460
+ const flagTs = flagResult?.result?.value;
2461
+ if (flagTs && flagTs > startTime) {
2462
+ console.log(`[browse] DOM mutation detected in opt pass (${elapsed}s) — aborting`);
2463
+ controller.abort();
2464
+ return;
2465
+ }
2466
+ }
2467
+ catch { /* page disconnected */ }
2468
+ console.log(`[browse] Optimization pass waiting... (${elapsed}s)`);
2469
+ }, 5000);
2222
2470
  try {
2223
2471
  const res = await fetch(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
2224
2472
  method: 'POST',
@@ -2261,6 +2509,7 @@ async function analyzeScreenshotWithPrompt(baseUrl, apiKey, model, screenshotBas
2261
2509
  }
2262
2510
  finally {
2263
2511
  clearTimeout(timeout);
2512
+ clearInterval(heartbeat);
2264
2513
  }
2265
2514
  }
2266
2515
  function sleep(ms) {