chrometools-mcp 3.5.4 → 3.5.6
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/CHANGELOG.md +25 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -1217
- package/README.md +13 -4
- package/SPEC-swagger-api-tools.md +3101 -3101
- package/browser/page-manager.js +7 -0
- package/index.js +199 -14
- package/models/DATEPICKER_IMPLEMENTATION.md +543 -543
- package/models/ModelRegistry.js +115 -115
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +56 -1
- package/server/tool-definitions.js +10 -5
- package/server/tool-schemas.js +10 -5
- package/specs/SEGM-537-UNBLOCKERS_PROGRESS.md +94 -0
- package/specs/SEGM-537-UNBLOCKERS_SPEC.md +187 -0
- package/utils/actions/click-action.js +76 -8
- package/SPEC-IMPROVEMENTS.md +0 -173
- package/SPEC-pom-integration.md +0 -227
package/browser/page-manager.js
CHANGED
|
@@ -101,8 +101,14 @@ function cleanOldNetworkRequests() {
|
|
|
101
101
|
// Page analysis cache
|
|
102
102
|
export const pageAnalysisCache = new Map();
|
|
103
103
|
|
|
104
|
+
/** Get the CDP client used for network monitoring on a page (for Network.getResponseBody) */
|
|
105
|
+
export function getNetworkCDPClient(page) {
|
|
106
|
+
return networkCDPClients.get(page) || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
// Track pages with network monitoring to prevent duplicate setup
|
|
105
110
|
const pagesWithNetworkMonitoring = new WeakSet();
|
|
111
|
+
const networkCDPClients = new WeakMap();
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
114
|
* Setup network monitoring with auto-reinitialization on navigation
|
|
@@ -117,6 +123,7 @@ export async function setupNetworkMonitoring(page) {
|
|
|
117
123
|
|
|
118
124
|
const client = await page.target().createCDPSession();
|
|
119
125
|
await client.send('Network.enable');
|
|
126
|
+
networkCDPClients.set(page, client);
|
|
120
127
|
|
|
121
128
|
client.on('Network.requestWillBeSent', (event) => {
|
|
122
129
|
const timestamp = new Date().toISOString();
|
package/index.js
CHANGED
|
@@ -36,7 +36,8 @@ import {
|
|
|
36
36
|
getAndClearNewTabEvents,
|
|
37
37
|
getAllPages,
|
|
38
38
|
switchToPage,
|
|
39
|
-
connectToTabByUrl
|
|
39
|
+
connectToTabByUrl,
|
|
40
|
+
getNetworkCDPClient
|
|
40
41
|
} from './browser/page-manager.js';
|
|
41
42
|
|
|
42
43
|
// Import image processing utilities
|
|
@@ -402,8 +403,13 @@ async function executeToolInternal(name, args) {
|
|
|
402
403
|
* Quick element registration - runs APOM analysis and registers elements
|
|
403
404
|
*/
|
|
404
405
|
async function quickRegisterElements(page) {
|
|
405
|
-
await page.evaluate((apomTreeConverterCode, selectorResolverCode) => {
|
|
406
|
-
// Inject utilities
|
|
406
|
+
await page.evaluate((apomTreeConverterCode, selectorResolverCode, modelsCode) => {
|
|
407
|
+
// Inject utilities. Models must be loaded BEFORE buildAPOMTree because
|
|
408
|
+
// initializeModelRegistry() inside it references window.ModelRegistry. After a
|
|
409
|
+
// navigation the browser window is wiped, so we always re-eval if missing.
|
|
410
|
+
if (typeof window.ModelRegistry === 'undefined') {
|
|
411
|
+
eval(modelsCode);
|
|
412
|
+
}
|
|
407
413
|
if (typeof buildAPOMTree === 'undefined') {
|
|
408
414
|
eval(apomTreeConverterCode);
|
|
409
415
|
}
|
|
@@ -440,7 +446,7 @@ async function executeToolInternal(name, args) {
|
|
|
440
446
|
if (typeof registerElements !== 'undefined') {
|
|
441
447
|
registerElements(elementsArray);
|
|
442
448
|
}
|
|
443
|
-
}, apomTreeConverter, selectorResolver);
|
|
449
|
+
}, apomTreeConverter, selectorResolver, elementModelBase + '\n' + elementModels + '\n' + modelRegistry);
|
|
444
450
|
}
|
|
445
451
|
|
|
446
452
|
/**
|
|
@@ -469,15 +475,56 @@ async function executeToolInternal(name, args) {
|
|
|
469
475
|
|
|
470
476
|
let resolved = await Promise.race([tryResolve(), timeoutPromise]);
|
|
471
477
|
|
|
472
|
-
// Auto-refresh: if looks like APOM ID but not found, re-register elements and retry
|
|
478
|
+
// Auto-refresh: if looks like APOM ID but not found, re-register elements and retry.
|
|
479
|
+
// quickRegisterElements re-injects ModelRegistry — if that itself fails (e.g. ReferenceError
|
|
480
|
+
// from a partially-loaded browser context), surface a clear "call analyzePage" message
|
|
481
|
+
// instead of leaking the raw browser error.
|
|
473
482
|
if (!resolved.found && isApomIdPattern(identifier)) {
|
|
474
|
-
|
|
483
|
+
try {
|
|
484
|
+
await quickRegisterElements(page);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
if (/ModelRegistry is not defined/.test(String(e?.message || e))) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`APOM registry stale after navigation/reload. Call analyzePage() to refresh element ids before reusing "${identifier}".`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
throw e;
|
|
492
|
+
}
|
|
475
493
|
resolved = await Promise.race([tryResolve(), timeoutPromise]);
|
|
476
494
|
}
|
|
477
495
|
|
|
478
496
|
return resolved;
|
|
479
497
|
}
|
|
480
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Snapshot of all APOM ids on the page → { id: { tag, type, text } }
|
|
501
|
+
* Used by click({ autoAnalyzeAfter: true }) to compute pre/post deltas.
|
|
502
|
+
*/
|
|
503
|
+
async function getApomSnapshot(page) {
|
|
504
|
+
return await page.evaluate((apomCode, modelsCode) => {
|
|
505
|
+
if (typeof window.ModelRegistry === 'undefined') eval(modelsCode);
|
|
506
|
+
if (typeof buildAPOMTree === 'undefined') eval(apomCode);
|
|
507
|
+
const apom = buildAPOMTree(true);
|
|
508
|
+
const map = {};
|
|
509
|
+
function walk(node) {
|
|
510
|
+
if (!node || typeof node !== 'object') return;
|
|
511
|
+
if (node.id) {
|
|
512
|
+
map[node.id] = {
|
|
513
|
+
tag: node.tag,
|
|
514
|
+
type: node.type,
|
|
515
|
+
text: (node.metadata && node.metadata.text ? String(node.metadata.text).substring(0, 60) : null)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
if (Array.isArray(node.children)) node.children.forEach(walk);
|
|
519
|
+
for (const k of Object.keys(node)) {
|
|
520
|
+
if (Array.isArray(node[k])) node[k].forEach(walk);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
walk(apom.tree);
|
|
524
|
+
return map;
|
|
525
|
+
}, apomTreeConverter, elementModelBase + '\n' + elementModels + '\n' + modelRegistry);
|
|
526
|
+
}
|
|
527
|
+
|
|
481
528
|
if (name === "click") {
|
|
482
529
|
const validatedArgs = schemas.ClickSchema.parse(args);
|
|
483
530
|
const page = await getLastOpenPage();
|
|
@@ -499,13 +546,61 @@ async function executeToolInternal(name, args) {
|
|
|
499
546
|
throw new Error(`Element not found: ${identifier}`);
|
|
500
547
|
}
|
|
501
548
|
|
|
549
|
+
// Pre-click APOM snapshot (only if delta is requested) — captures every id
|
|
550
|
+
// currently in the tree so we can diff the post-click state.
|
|
551
|
+
let preSnap = null;
|
|
552
|
+
if (validatedArgs.autoAnalyzeAfter) {
|
|
553
|
+
preSnap = await getApomSnapshot(page);
|
|
554
|
+
}
|
|
555
|
+
|
|
502
556
|
// Use shared click action handler
|
|
503
|
-
|
|
557
|
+
const clickResult = await executeClickAction(page, element, {
|
|
504
558
|
identifier,
|
|
505
559
|
screenshot: validatedArgs.screenshot,
|
|
506
560
|
skipNetworkWait: validatedArgs.skipNetworkWait,
|
|
507
|
-
networkWaitTimeout: validatedArgs.networkWaitTimeout
|
|
561
|
+
networkWaitTimeout: validatedArgs.networkWaitTimeout,
|
|
562
|
+
waitForSelector: validatedArgs.waitForSelector,
|
|
563
|
+
waitTimeoutMs: validatedArgs.waitTimeoutMs
|
|
508
564
|
});
|
|
565
|
+
|
|
566
|
+
// Post-click APOM delta: re-register new ids so callers can immediately
|
|
567
|
+
// use them in follow-up click/type calls without an extra analyzePage call.
|
|
568
|
+
if (validatedArgs.autoAnalyzeAfter && preSnap) {
|
|
569
|
+
try {
|
|
570
|
+
await quickRegisterElements(page);
|
|
571
|
+
const postSnap = await getApomSnapshot(page);
|
|
572
|
+
const added = Object.keys(postSnap).filter(id => !preSnap[id]);
|
|
573
|
+
const removed = Object.keys(preSnap).filter(id => !postSnap[id]);
|
|
574
|
+
|
|
575
|
+
let deltaText = '\n\n** APOM DELTA **';
|
|
576
|
+
if (added.length === 0 && removed.length === 0) {
|
|
577
|
+
deltaText += '\nNo APOM changes detected after click';
|
|
578
|
+
} else {
|
|
579
|
+
if (added.length > 0) {
|
|
580
|
+
const sample = added.slice(0, 15).map(id => {
|
|
581
|
+
const m = postSnap[id] || {};
|
|
582
|
+
const lab = m.text ? `"${m.text}"` : (m.type || m.tag || '?');
|
|
583
|
+
return `${id}:${lab}`;
|
|
584
|
+
});
|
|
585
|
+
deltaText += `\n+${added.length} appeared: ${sample.join(', ')}${added.length > 15 ? `, ... (${added.length - 15} more)` : ''}`;
|
|
586
|
+
}
|
|
587
|
+
if (removed.length > 0) {
|
|
588
|
+
deltaText += `\n-${removed.length} disappeared`;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (clickResult && Array.isArray(clickResult.content) && clickResult.content[0]) {
|
|
593
|
+
clickResult.content[0].text += deltaText;
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
// Delta is best-effort — never let it fail the actual click result
|
|
597
|
+
if (clickResult && Array.isArray(clickResult.content) && clickResult.content[0]) {
|
|
598
|
+
clickResult.content[0].text += `\n\n** APOM DELTA ** unavailable (${e.message})`;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return clickResult;
|
|
509
604
|
};
|
|
510
605
|
|
|
511
606
|
// Execute with timeout
|
|
@@ -785,6 +880,24 @@ async function executeToolInternal(name, args) {
|
|
|
785
880
|
// Get identifier (id or selector)
|
|
786
881
|
const identifier = validatedArgs.id || validatedArgs.selector;
|
|
787
882
|
|
|
883
|
+
// No identifier → full viewport screenshot. Mirrors processSceenshot pipeline used
|
|
884
|
+
// by element screenshots so the output (JPEG, scaled, base64) stays consistent.
|
|
885
|
+
if (!identifier) {
|
|
886
|
+
const buffer = await page.screenshot({ encoding: 'binary', fullPage: false });
|
|
887
|
+
const processed = await processScreenshot(buffer, {
|
|
888
|
+
maxWidth: validatedArgs.maxWidth !== undefined ? validatedArgs.maxWidth : 1024,
|
|
889
|
+
maxHeight: validatedArgs.maxHeight !== undefined ? validatedArgs.maxHeight : 8000,
|
|
890
|
+
quality: validatedArgs.quality || 40,
|
|
891
|
+
format: validatedArgs.format || 'jpeg',
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
content: [
|
|
895
|
+
{ type: 'text', text: 'Viewport screenshot' },
|
|
896
|
+
{ type: 'image', data: processed.buffer.toString('base64'), mimeType: processed.mimeType }
|
|
897
|
+
]
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
788
901
|
// Resolve selector (supports both APOM ID and CSS selector)
|
|
789
902
|
const resolved = await resolveSelector(page, identifier);
|
|
790
903
|
if (!resolved.found) {
|
|
@@ -939,17 +1052,34 @@ async function executeToolInternal(name, args) {
|
|
|
939
1052
|
const page = await getLastOpenPage();
|
|
940
1053
|
const timeout = validatedArgs.timeout || 30000;
|
|
941
1054
|
|
|
1055
|
+
// Auto-wrap top-level `return` into an async IIFE: `eval('return X')` is an
|
|
1056
|
+
// Illegal return statement in script context, so users had to wrap manually.
|
|
1057
|
+
// Heuristic: rewrite only when the snippet starts with `return ...` (most common
|
|
1058
|
+
// case from QA reports). Skip when the snippet declares a function — that signals
|
|
1059
|
+
// an explicit user-defined scope and an implicit-return result is likely intended.
|
|
1060
|
+
let scriptToRun = validatedArgs.script;
|
|
1061
|
+
const trimmedHead = scriptToRun.replace(/^\s+/, '');
|
|
1062
|
+
if (/^return[\s;]/.test(trimmedHead) && !/\bfunction\s*[\w*(]/.test(scriptToRun)) {
|
|
1063
|
+
scriptToRun = `(async () => { ${scriptToRun} })()`;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
942
1066
|
// Wrap operation in timeout
|
|
943
1067
|
const executeOperation = async () => {
|
|
944
|
-
|
|
1068
|
+
// Use page.evaluate with async support — if eval returns a Promise,
|
|
1069
|
+
// await it so async IIFEs work correctly instead of returning {}
|
|
1070
|
+
const result = await page.evaluate(async (code) => {
|
|
945
1071
|
try {
|
|
946
1072
|
// eslint-disable-next-line no-eval
|
|
947
|
-
|
|
1073
|
+
let evalResult = eval(code);
|
|
1074
|
+
// Await promises (async IIFE, fetch, etc.)
|
|
1075
|
+
if (evalResult && typeof evalResult === 'object' && typeof evalResult.then === 'function') {
|
|
1076
|
+
evalResult = await evalResult;
|
|
1077
|
+
}
|
|
948
1078
|
return { success: true, result: evalResult };
|
|
949
1079
|
} catch (error) {
|
|
950
1080
|
return { success: false, error: error.message };
|
|
951
1081
|
}
|
|
952
|
-
},
|
|
1082
|
+
}, scriptToRun);
|
|
953
1083
|
|
|
954
1084
|
await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 500));
|
|
955
1085
|
|
|
@@ -1083,6 +1213,31 @@ async function executeToolInternal(name, args) {
|
|
|
1083
1213
|
}
|
|
1084
1214
|
};
|
|
1085
1215
|
|
|
1216
|
+
// Retrieve response body via CDP using the same session that captured the request
|
|
1217
|
+
let responseBody = undefined;
|
|
1218
|
+
if (req.finishedTimestamp && !req.errorText) {
|
|
1219
|
+
try {
|
|
1220
|
+
const page = await getLastOpenPage();
|
|
1221
|
+
const client = getNetworkCDPClient(page);
|
|
1222
|
+
if (client) {
|
|
1223
|
+
const { body, base64Encoded } = await client.send('Network.getResponseBody', { requestId: req.requestId });
|
|
1224
|
+
if (base64Encoded) {
|
|
1225
|
+
responseBody = `[base64 encoded, ${body.length} chars]`;
|
|
1226
|
+
} else {
|
|
1227
|
+
// Minify JSON responses, truncate large bodies
|
|
1228
|
+
const maxBodySize = 50000;
|
|
1229
|
+
responseBody = body.length > maxBodySize
|
|
1230
|
+
? minifyJson(body.substring(0, maxBodySize)) + `... [truncated, total ${body.length} chars]`
|
|
1231
|
+
: minifyJson(body);
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
responseBody = '[unavailable: no network CDP session]';
|
|
1235
|
+
}
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
responseBody = `[unavailable: ${e.message}]`;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1086
1241
|
const result = {
|
|
1087
1242
|
requestId: req.requestId,
|
|
1088
1243
|
url: req.url,
|
|
@@ -1098,6 +1253,7 @@ async function executeToolInternal(name, args) {
|
|
|
1098
1253
|
headers: req.headers,
|
|
1099
1254
|
postData: req.postData ? minifyJson(req.postData) : undefined,
|
|
1100
1255
|
responseHeaders: req.responseHeaders,
|
|
1256
|
+
responseBody,
|
|
1101
1257
|
mimeType: req.mimeType,
|
|
1102
1258
|
errorText: req.errorText,
|
|
1103
1259
|
canceled: req.canceled,
|
|
@@ -2361,8 +2517,34 @@ Start coding now.`;
|
|
|
2361
2517
|
const page = await getLastOpenPage();
|
|
2362
2518
|
const pageUrl = page.url();
|
|
2363
2519
|
|
|
2520
|
+
// Check for non-HTML pages (JSON, plain text, XML) — return raw content instead of empty tree
|
|
2521
|
+
const contentType = await page.evaluate(() => document.contentType || '');
|
|
2522
|
+
if (contentType && !contentType.includes('html')) {
|
|
2523
|
+
const rawContent = await page.evaluate(() => {
|
|
2524
|
+
// For JSON/text pages, browser wraps content in <pre> inside <body>
|
|
2525
|
+
const pre = document.querySelector('pre');
|
|
2526
|
+
const text = pre ? pre.textContent : document.body?.innerText || '';
|
|
2527
|
+
// Truncate large content
|
|
2528
|
+
const maxSize = 50000;
|
|
2529
|
+
return text.length > maxSize ? text.substring(0, maxSize) + `\n... [truncated, total ${text.length} chars]` : text;
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
return {
|
|
2533
|
+
content: [{
|
|
2534
|
+
type: 'text',
|
|
2535
|
+
text: JSON.stringify({
|
|
2536
|
+
pageId: `page_${Buffer.from(pageUrl).toString('base64').substring(0, 20)}_${Date.now()}`,
|
|
2537
|
+
url: pageUrl,
|
|
2538
|
+
contentType,
|
|
2539
|
+
rawContent,
|
|
2540
|
+
metadata: { totalElements: 0, interactiveCount: 0, nonHtmlPage: true }
|
|
2541
|
+
})
|
|
2542
|
+
}]
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2364
2546
|
// APOM Tree format (default) - v2 with tree structure and positioning
|
|
2365
|
-
const apomResult = await page.evaluate(async (apomTreeConverterCode, selectorResolverCode, modelsCode, shouldRegister, includeAll, viewportOnly) => {
|
|
2547
|
+
const apomResult = await page.evaluate(async (apomTreeConverterCode, selectorResolverCode, modelsCode, shouldRegister, includeAll, viewportOnly, portalOpts) => {
|
|
2366
2548
|
// Inject utilities if not already loaded
|
|
2367
2549
|
if (typeof buildAPOMTree === 'undefined') {
|
|
2368
2550
|
eval(apomTreeConverterCode);
|
|
@@ -2399,7 +2581,7 @@ Start coding now.`;
|
|
|
2399
2581
|
|
|
2400
2582
|
// Build APOM tree
|
|
2401
2583
|
// interactiveOnly = !includeAll (if includeAll is true, we want ALL elements)
|
|
2402
|
-
const apomData = buildAPOMTree(!includeAll, viewportOnly);
|
|
2584
|
+
const apomData = buildAPOMTree(!includeAll, viewportOnly, portalOpts);
|
|
2403
2585
|
|
|
2404
2586
|
// Register elements in selector resolver if requested
|
|
2405
2587
|
if (shouldRegister) {
|
|
@@ -2432,7 +2614,10 @@ Start coding now.`;
|
|
|
2432
2614
|
}
|
|
2433
2615
|
|
|
2434
2616
|
return apomData;
|
|
2435
|
-
}, apomTreeConverter, selectorResolver, elementModelBase + '\n' + elementModels + '\n' + modelRegistry, validatedArgs.registerElements !== false, validatedArgs.includeAll || false, validatedArgs.viewportOnly || false
|
|
2617
|
+
}, apomTreeConverter, selectorResolver, elementModelBase + '\n' + elementModels + '\n' + modelRegistry, validatedArgs.registerElements !== false, validatedArgs.includeAll || false, validatedArgs.viewportOnly || false, {
|
|
2618
|
+
include: validatedArgs.includePortals !== false,
|
|
2619
|
+
selectors: validatedArgs.portalSelectors || undefined
|
|
2620
|
+
});
|
|
2436
2621
|
|
|
2437
2622
|
// Handle diff mode
|
|
2438
2623
|
if (validatedArgs.diff) {
|