teamshare-bridge 0.19.23 → 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.
- package/dist/cli/commands/browse.js +308 -59
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/build.js +31 -2
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/run.js +30 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/worker.js +33 -1
- package/dist/cli/commands/worker.js.map +1 -1
- package/dist/lib/git/git-runner.d.ts +55 -0
- package/dist/lib/git/git-runner.js +323 -0
- package/dist/lib/git/git-runner.js.map +1 -0
- package/dist/lib/git/index.d.ts +2 -0
- package/dist/lib/git/index.js +5 -1
- package/dist/lib/git/index.js.map +1 -1
- package/dist/lib/run-report.d.ts +3 -0
- package/dist/lib/run-report.js +4 -0
- package/dist/lib/run-report.js.map +1 -1
- package/package.json +1 -1
- package/dist/bridge/register-only.d.ts +0 -2
- package/dist/bridge/register-only.js +0 -104
- package/dist/bridge/register-only.js.map +0 -1
- package/dist/lib/agy-pty.d.ts +0 -25
- package/dist/lib/agy-pty.js +0 -114
- package/dist/lib/agy-pty.js.map +0 -1
- package/dist/lib/chat-lock.d.ts +0 -8
- package/dist/lib/chat-lock.js +0 -59
- package/dist/lib/chat-lock.js.map +0 -1
- package/dist/lib/chat-reply.d.ts +0 -71
- package/dist/lib/chat-reply.js +0 -424
- package/dist/lib/chat-reply.js.map +0 -1
- package/dist/lib/daemon-reporter.d.ts +0 -17
- package/dist/lib/daemon-reporter.js +0 -125
- package/dist/lib/daemon-reporter.js.map +0 -1
- package/dist/lib/doc-reply.d.ts +0 -54
- package/dist/lib/doc-reply.js +0 -278
- package/dist/lib/doc-reply.js.map +0 -1
- package/dist/lib/git-auth.d.ts +0 -15
- package/dist/lib/git-auth.js +0 -92
- package/dist/lib/git-auth.js.map +0 -1
- package/dist/lib/git-auto.d.ts +0 -29
- package/dist/lib/git-auto.js +0 -141
- package/dist/lib/git-auto.js.map +0 -1
- package/dist/lib/git-pipeline.d.ts +0 -161
- package/dist/lib/git-pipeline.js +0 -598
- package/dist/lib/git-pipeline.js.map +0 -1
- package/dist/lib/harness-adapter.d.ts +0 -92
- package/dist/lib/harness-adapter.js +0 -735
- package/dist/lib/harness-adapter.js.map +0 -1
- package/dist/lib/harness-models.d.ts +0 -17
- package/dist/lib/harness-models.js +0 -150
- package/dist/lib/harness-models.js.map +0 -1
- package/dist/lib/harness-output.d.ts +0 -31
- package/dist/lib/harness-output.js +0 -270
- package/dist/lib/harness-output.js.map +0 -1
- package/dist/lib/harness.d.ts +0 -82
- package/dist/lib/harness.js +0 -362
- package/dist/lib/harness.js.map +0 -1
- package/dist/lib/llm.d.ts +0 -91
- package/dist/lib/llm.js +0 -831
- package/dist/lib/llm.js.map +0 -1
- package/dist/lib/models.d.ts +0 -51
- package/dist/lib/models.js +0 -124
- package/dist/lib/models.js.map +0 -1
- package/dist/lib/pr-create.d.ts +0 -30
- package/dist/lib/pr-create.js +0 -83
- package/dist/lib/pr-create.js.map +0 -1
- package/dist/lib/prompt-file.d.ts +0 -7
- package/dist/lib/prompt-file.js +0 -69
- package/dist/lib/prompt-file.js.map +0 -1
- package/dist/lib/retry.d.ts +0 -21
- package/dist/lib/retry.js +0 -60
- package/dist/lib/retry.js.map +0 -1
- package/dist/lib/session-stream.d.ts +0 -38
- package/dist/lib/session-stream.js +0 -204
- package/dist/lib/session-stream.js.map +0 -1
- package/dist/lib/slug.d.ts +0 -5
- package/dist/lib/slug.js +0 -18
- package/dist/lib/slug.js.map +0 -1
- package/dist/lib/usage-reader.d.ts +0 -46
- package/dist/lib/usage-reader.js +0 -206
- package/dist/lib/usage-reader.js.map +0 -1
- package/dist/lib/usage.d.ts +0 -18
- package/dist/lib/usage.js +0 -30
- 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') ?? '
|
|
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 >=
|
|
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 >=
|
|
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
|
|
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
|
-
|
|
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:
|
|
1167
|
+
type: 'mousePressed', x: clickX, y: clickY, button: 'left', clickCount: 1,
|
|
994
1168
|
});
|
|
995
1169
|
await cdpSend(page, 'Input.dispatchMouseEvent', {
|
|
996
|
-
type: 'mouseReleased', x:
|
|
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
|
-
|
|
1117
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 +=
|
|
1703
|
+
delay += 30 + Math.random() * 80;
|
|
1704
|
+
// Digits: slight pause
|
|
1523
1705
|
if (/[0-9]/.test(char))
|
|
1524
|
-
delay +=
|
|
1706
|
+
delay += 15 + Math.random() * 40;
|
|
1707
|
+
// Special chars: longer pause (reaching for symbol)
|
|
1525
1708
|
if (/^[{}():"<>!@#$%^&*_+|~`?\[\]]$/.test(char))
|
|
1526
|
-
delay +=
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
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) {
|