@react-grab/cli 0.1.0-beta.3 → 0.1.0-beta.4
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.cjs +276 -84
- package/dist/cli.js +276 -84
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -2707,7 +2707,7 @@ var previewPackageJsonAgentRemoval = (projectRoot, agent) => {
|
|
|
2707
2707
|
};
|
|
2708
2708
|
|
|
2709
2709
|
// src/commands/add.ts
|
|
2710
|
-
var VERSION = "0.1.0";
|
|
2710
|
+
var VERSION = "0.1.0-beta.3";
|
|
2711
2711
|
var add = new commander.Command().name("add").alias("install").description("add an agent integration or MCP server").argument(
|
|
2712
2712
|
"[agent]",
|
|
2713
2713
|
`agent to add (${AGENTS.join(", ")}, mcp, skill)`
|
|
@@ -3236,6 +3236,15 @@ var add = new commander.Command().name("add").alias("install").description("add
|
|
|
3236
3236
|
handleError(error48);
|
|
3237
3237
|
}
|
|
3238
3238
|
});
|
|
3239
|
+
|
|
3240
|
+
// src/utils/constants.ts
|
|
3241
|
+
var MAX_SUGGESTIONS_COUNT = 30;
|
|
3242
|
+
var MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
3243
|
+
var MAX_CONTEXT_LINES = 50;
|
|
3244
|
+
var COMPONENT_STACK_MAX_DEPTH = 10;
|
|
3245
|
+
|
|
3246
|
+
// src/utils/browser-automation.ts
|
|
3247
|
+
var LOAD_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
|
|
3239
3248
|
var ensureHealthyServer = async (options2 = {}) => {
|
|
3240
3249
|
const cliPath = process.argv[1];
|
|
3241
3250
|
const serverRunning = await browser$1.isServerRunning();
|
|
@@ -3266,16 +3275,65 @@ var getOrCreatePage = async (serverUrl, name) => {
|
|
|
3266
3275
|
}
|
|
3267
3276
|
return response.json();
|
|
3268
3277
|
};
|
|
3278
|
+
var getReactContextForActiveElement = async (page) => {
|
|
3279
|
+
try {
|
|
3280
|
+
return page.evaluate(async (maxDepth) => {
|
|
3281
|
+
const activeElement = document.activeElement;
|
|
3282
|
+
if (!activeElement || activeElement === document.body) return null;
|
|
3283
|
+
const reactGrab = globalThis.__REACT_GRAB__;
|
|
3284
|
+
if (!reactGrab?.getSource) return null;
|
|
3285
|
+
const source = await reactGrab.getSource(activeElement);
|
|
3286
|
+
if (!source) return null;
|
|
3287
|
+
const componentStack = [];
|
|
3288
|
+
if (source.componentName) {
|
|
3289
|
+
componentStack.push(source.componentName);
|
|
3290
|
+
}
|
|
3291
|
+
const fiberKey = Object.keys(activeElement).find(
|
|
3292
|
+
(key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
|
|
3293
|
+
);
|
|
3294
|
+
if (fiberKey) {
|
|
3295
|
+
let fiber = activeElement[fiberKey];
|
|
3296
|
+
let depth = 0;
|
|
3297
|
+
while (fiber?.return && depth < maxDepth) {
|
|
3298
|
+
fiber = fiber.return;
|
|
3299
|
+
if (fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 11) {
|
|
3300
|
+
const name = typeof fiber.type === "object" ? fiber.type?.displayName || fiber.type?.name : null;
|
|
3301
|
+
if (name && !name.startsWith("_") && !componentStack.includes(name)) {
|
|
3302
|
+
componentStack.push(name);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
depth++;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return {
|
|
3309
|
+
element: activeElement.tagName.toLowerCase(),
|
|
3310
|
+
component: source.componentName || void 0,
|
|
3311
|
+
source: source.filePath ? `${source.filePath}${source.lineNumber ? `:${source.lineNumber}` : ""}` : void 0,
|
|
3312
|
+
componentStack: componentStack.length > 0 ? componentStack : void 0
|
|
3313
|
+
};
|
|
3314
|
+
}, COMPONENT_STACK_MAX_DEPTH);
|
|
3315
|
+
} catch {
|
|
3316
|
+
return null;
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3269
3319
|
var createOutputJson = (getPage, pageName) => {
|
|
3270
3320
|
return async (ok, result, error48) => {
|
|
3271
3321
|
const page = getPage();
|
|
3322
|
+
let reactContext;
|
|
3323
|
+
if (page && ok) {
|
|
3324
|
+
const context = await getReactContextForActiveElement(page);
|
|
3325
|
+
if (context) {
|
|
3326
|
+
reactContext = context;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3272
3329
|
return {
|
|
3273
3330
|
ok,
|
|
3274
3331
|
url: page?.url() ?? "",
|
|
3275
3332
|
title: page ? await page.title().catch(() => "") : "",
|
|
3276
3333
|
page: pageName,
|
|
3277
3334
|
...result !== void 0 && { result },
|
|
3278
|
-
...error48 && { error: error48 }
|
|
3335
|
+
...error48 && { error: error48 },
|
|
3336
|
+
...reactContext && { reactContext }
|
|
3279
3337
|
};
|
|
3280
3338
|
};
|
|
3281
3339
|
};
|
|
@@ -3334,6 +3392,24 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3334
3392
|
return g2.__REACT_GRAB__.getSource(el);
|
|
3335
3393
|
}, element);
|
|
3336
3394
|
};
|
|
3395
|
+
const getProps = async (refId) => {
|
|
3396
|
+
const element = await getElement(refId);
|
|
3397
|
+
const currentPage2 = getActivePage2();
|
|
3398
|
+
return currentPage2.evaluate((el) => {
|
|
3399
|
+
const g2 = globalThis;
|
|
3400
|
+
if (!g2.__REACT_GRAB_GET_PROPS__) return null;
|
|
3401
|
+
return g2.__REACT_GRAB_GET_PROPS__(el);
|
|
3402
|
+
}, element);
|
|
3403
|
+
};
|
|
3404
|
+
const getState = async (refId) => {
|
|
3405
|
+
const element = await getElement(refId);
|
|
3406
|
+
const currentPage2 = getActivePage2();
|
|
3407
|
+
return currentPage2.evaluate((el) => {
|
|
3408
|
+
const g2 = globalThis;
|
|
3409
|
+
if (!g2.__REACT_GRAB_GET_STATE__) return null;
|
|
3410
|
+
return g2.__REACT_GRAB_GET_STATE__(el);
|
|
3411
|
+
}, element);
|
|
3412
|
+
};
|
|
3337
3413
|
return (refId) => {
|
|
3338
3414
|
return new Proxy(
|
|
3339
3415
|
{},
|
|
@@ -3345,6 +3421,12 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3345
3421
|
if (prop === "source") {
|
|
3346
3422
|
return () => getSource(refId);
|
|
3347
3423
|
}
|
|
3424
|
+
if (prop === "props") {
|
|
3425
|
+
return () => getProps(refId);
|
|
3426
|
+
}
|
|
3427
|
+
if (prop === "state") {
|
|
3428
|
+
return () => getState(refId);
|
|
3429
|
+
}
|
|
3348
3430
|
if (prop === "screenshot") {
|
|
3349
3431
|
return (options2) => getElement(refId).then(
|
|
3350
3432
|
(el) => el.screenshot({ scale: "css", ...options2 })
|
|
@@ -3358,6 +3440,50 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3358
3440
|
);
|
|
3359
3441
|
};
|
|
3360
3442
|
};
|
|
3443
|
+
var createComponentHelper = (getActivePage2) => {
|
|
3444
|
+
return async (componentName, options2) => {
|
|
3445
|
+
const currentPage2 = getActivePage2();
|
|
3446
|
+
const nth = options2?.nth;
|
|
3447
|
+
const elementHandles = await currentPage2.evaluateHandle(
|
|
3448
|
+
async (args) => {
|
|
3449
|
+
const g2 = globalThis;
|
|
3450
|
+
if (!g2.__REACT_GRAB_FIND_BY_COMPONENT__) {
|
|
3451
|
+
throw new Error("React introspection not available. Make sure react-grab is installed.");
|
|
3452
|
+
}
|
|
3453
|
+
const result = await g2.__REACT_GRAB_FIND_BY_COMPONENT__(args.name, args.nth !== void 0 ? { nth: args.nth } : void 0);
|
|
3454
|
+
if (!result) return null;
|
|
3455
|
+
if (args.nth !== void 0) {
|
|
3456
|
+
const single = result;
|
|
3457
|
+
return single?.element || null;
|
|
3458
|
+
}
|
|
3459
|
+
const arr = result;
|
|
3460
|
+
return arr.map((m2) => m2.element);
|
|
3461
|
+
},
|
|
3462
|
+
{ name: componentName, nth }
|
|
3463
|
+
);
|
|
3464
|
+
const value = await elementHandles.jsonValue().catch(() => null);
|
|
3465
|
+
if (value === null) {
|
|
3466
|
+
await elementHandles.dispose();
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
if (nth !== void 0) {
|
|
3470
|
+
const element = elementHandles.asElement();
|
|
3471
|
+
if (!element) {
|
|
3472
|
+
await elementHandles.dispose();
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
return element;
|
|
3476
|
+
}
|
|
3477
|
+
const jsHandles = await elementHandles.getProperties();
|
|
3478
|
+
const handles = [];
|
|
3479
|
+
for (const [, handle] of jsHandles) {
|
|
3480
|
+
const element = handle.asElement();
|
|
3481
|
+
if (element) handles.push(element);
|
|
3482
|
+
}
|
|
3483
|
+
await elementHandles.dispose();
|
|
3484
|
+
return handles;
|
|
3485
|
+
};
|
|
3486
|
+
};
|
|
3361
3487
|
var createFillHelper = (ref, getActivePage2) => {
|
|
3362
3488
|
return async (refId, text) => {
|
|
3363
3489
|
const element = await ref(refId);
|
|
@@ -3459,35 +3585,22 @@ var createDispatchHelper = (getActivePage2) => {
|
|
|
3459
3585
|
};
|
|
3460
3586
|
};
|
|
3461
3587
|
var createGrabHelper = (ref, getActivePage2) => {
|
|
3588
|
+
const evaluateGrabMethod = async (methodName, defaultValue) => {
|
|
3589
|
+
const currentPage2 = getActivePage2();
|
|
3590
|
+
return currentPage2.evaluate(
|
|
3591
|
+
({ method, fallback }) => {
|
|
3592
|
+
const grab = globalThis.__REACT_GRAB__;
|
|
3593
|
+
return grab?.[method]?.() ?? fallback;
|
|
3594
|
+
},
|
|
3595
|
+
{ method: methodName, fallback: defaultValue }
|
|
3596
|
+
);
|
|
3597
|
+
};
|
|
3462
3598
|
return {
|
|
3463
|
-
activate:
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
});
|
|
3469
|
-
},
|
|
3470
|
-
deactivate: async () => {
|
|
3471
|
-
const currentPage2 = getActivePage2();
|
|
3472
|
-
await currentPage2.evaluate(() => {
|
|
3473
|
-
const g2 = globalThis;
|
|
3474
|
-
g2.__REACT_GRAB__?.deactivate();
|
|
3475
|
-
});
|
|
3476
|
-
},
|
|
3477
|
-
toggle: async () => {
|
|
3478
|
-
const currentPage2 = getActivePage2();
|
|
3479
|
-
await currentPage2.evaluate(() => {
|
|
3480
|
-
const g2 = globalThis;
|
|
3481
|
-
g2.__REACT_GRAB__?.toggle();
|
|
3482
|
-
});
|
|
3483
|
-
},
|
|
3484
|
-
isActive: async () => {
|
|
3485
|
-
const currentPage2 = getActivePage2();
|
|
3486
|
-
return currentPage2.evaluate(() => {
|
|
3487
|
-
const g2 = globalThis;
|
|
3488
|
-
return g2.__REACT_GRAB__?.isActive() ?? false;
|
|
3489
|
-
});
|
|
3490
|
-
},
|
|
3599
|
+
activate: () => evaluateGrabMethod("activate", void 0),
|
|
3600
|
+
deactivate: () => evaluateGrabMethod("deactivate", void 0),
|
|
3601
|
+
toggle: () => evaluateGrabMethod("toggle", void 0),
|
|
3602
|
+
isActive: () => evaluateGrabMethod("isActive", false),
|
|
3603
|
+
getState: () => evaluateGrabMethod("getState", null),
|
|
3491
3604
|
copyElement: async (refId) => {
|
|
3492
3605
|
const element = await ref(refId);
|
|
3493
3606
|
if (!element) return false;
|
|
@@ -3496,13 +3609,6 @@ var createGrabHelper = (ref, getActivePage2) => {
|
|
|
3496
3609
|
const g2 = globalThis;
|
|
3497
3610
|
return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
|
|
3498
3611
|
}, element);
|
|
3499
|
-
},
|
|
3500
|
-
getState: async () => {
|
|
3501
|
-
const currentPage2 = getActivePage2();
|
|
3502
|
-
return currentPage2.evaluate(() => {
|
|
3503
|
-
const g2 = globalThis;
|
|
3504
|
-
return g2.__REACT_GRAB__?.getState() ?? null;
|
|
3505
|
-
});
|
|
3506
3612
|
}
|
|
3507
3613
|
};
|
|
3508
3614
|
};
|
|
@@ -3518,7 +3624,7 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3518
3624
|
return async (selectorOrState, options2) => {
|
|
3519
3625
|
const currentPage2 = getActivePage2();
|
|
3520
3626
|
const timeout = options2?.timeout;
|
|
3521
|
-
if (selectorOrState
|
|
3627
|
+
if (LOAD_STATES.has(selectorOrState)) {
|
|
3522
3628
|
await currentPage2.waitForLoadState(selectorOrState, { timeout });
|
|
3523
3629
|
return;
|
|
3524
3630
|
}
|
|
@@ -3529,6 +3635,33 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3529
3635
|
await currentPage2.waitForSelector(selectorOrState, { timeout });
|
|
3530
3636
|
};
|
|
3531
3637
|
};
|
|
3638
|
+
var connectToBrowserPage = async (pageName) => {
|
|
3639
|
+
const { chromium: chromium2 } = await import('playwright-core');
|
|
3640
|
+
const { findPageByTargetId: findPageByTargetId2 } = await import('@react-grab/browser');
|
|
3641
|
+
const { serverUrl } = await ensureHealthyServer();
|
|
3642
|
+
const pageInfo = await getOrCreatePage(serverUrl, pageName);
|
|
3643
|
+
const browser2 = await chromium2.connectOverCDP(pageInfo.wsEndpoint);
|
|
3644
|
+
const page = await findPageByTargetId2(browser2, pageInfo.targetId);
|
|
3645
|
+
if (!page) {
|
|
3646
|
+
await browser2.close();
|
|
3647
|
+
throw new Error(`Page "${pageName}" not found`);
|
|
3648
|
+
}
|
|
3649
|
+
return { browser: browser2, page, serverUrl };
|
|
3650
|
+
};
|
|
3651
|
+
var createMcpErrorResponse = (error48) => {
|
|
3652
|
+
return {
|
|
3653
|
+
content: [
|
|
3654
|
+
{
|
|
3655
|
+
type: "text",
|
|
3656
|
+
text: JSON.stringify({
|
|
3657
|
+
ok: false,
|
|
3658
|
+
error: error48 instanceof Error ? error48.message : "Failed"
|
|
3659
|
+
})
|
|
3660
|
+
}
|
|
3661
|
+
],
|
|
3662
|
+
isError: true
|
|
3663
|
+
};
|
|
3664
|
+
};
|
|
3532
3665
|
|
|
3533
3666
|
// ../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/external.js
|
|
3534
3667
|
var external_exports = {};
|
|
@@ -17298,7 +17431,13 @@ var startMcpServer = async () => {
|
|
|
17298
17431
|
server.registerTool(
|
|
17299
17432
|
"browser_snapshot",
|
|
17300
17433
|
{
|
|
17301
|
-
description: `Get ARIA accessibility tree with element refs (e1, e2...).
|
|
17434
|
+
description: `Get ARIA accessibility tree with element refs (e1, e2...) and React component info.
|
|
17435
|
+
|
|
17436
|
+
OUTPUT INCLUDES:
|
|
17437
|
+
- ARIA roles and accessible names
|
|
17438
|
+
- Element refs (e1, e2...) for interaction
|
|
17439
|
+
- [component=ComponentName] for React components
|
|
17440
|
+
- [source=file.tsx:line] for source location
|
|
17302
17441
|
|
|
17303
17442
|
SCREENSHOT STRATEGY - ALWAYS prefer element screenshots over full page:
|
|
17304
17443
|
1. First: Get refs with snapshot (this tool)
|
|
@@ -17316,7 +17455,7 @@ USE VIEWPORT screenshot=true ONLY FOR:
|
|
|
17316
17455
|
|
|
17317
17456
|
PERFORMANCE:
|
|
17318
17457
|
- interactableOnly:true = much smaller output (recommended)
|
|
17319
|
-
- format:'compact' = minimal ref:role:name output
|
|
17458
|
+
- format:'compact' = minimal ref:role:name@Component output
|
|
17320
17459
|
- maxDepth = limit tree depth
|
|
17321
17460
|
|
|
17322
17461
|
After getting refs, use browser_execute with: ref('e1').click()`,
|
|
@@ -17337,16 +17476,11 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17337
17476
|
format,
|
|
17338
17477
|
screenshot
|
|
17339
17478
|
}) => {
|
|
17340
|
-
let activePage = null;
|
|
17341
17479
|
let browser2 = null;
|
|
17342
17480
|
try {
|
|
17343
|
-
const
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
activePage = await browser$1.findPageByTargetId(browser2, pageInfo.targetId);
|
|
17347
|
-
if (!activePage) {
|
|
17348
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17349
|
-
}
|
|
17481
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17482
|
+
browser2 = connection.browser;
|
|
17483
|
+
const activePage = connection.page;
|
|
17350
17484
|
const getActivePage2 = () => activePage;
|
|
17351
17485
|
const snapshot = createSnapshotHelper(getActivePage2);
|
|
17352
17486
|
const snapshotResult = await snapshot({ maxDepth, interactableOnly, format });
|
|
@@ -17370,18 +17504,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17370
17504
|
content: [{ type: "text", text: snapshotResult }]
|
|
17371
17505
|
};
|
|
17372
17506
|
} catch (error48) {
|
|
17373
|
-
return
|
|
17374
|
-
content: [
|
|
17375
|
-
{
|
|
17376
|
-
type: "text",
|
|
17377
|
-
text: JSON.stringify({
|
|
17378
|
-
ok: false,
|
|
17379
|
-
error: error48 instanceof Error ? error48.message : "Failed"
|
|
17380
|
-
})
|
|
17381
|
-
}
|
|
17382
|
-
],
|
|
17383
|
-
isError: true
|
|
17384
|
-
};
|
|
17507
|
+
return createMcpErrorResponse(error48);
|
|
17385
17508
|
} finally {
|
|
17386
17509
|
await browser2?.close();
|
|
17387
17510
|
}
|
|
@@ -17396,25 +17519,32 @@ IMPORTANT: Always call snapshot() first to get element refs from the a11y tree (
|
|
|
17396
17519
|
|
|
17397
17520
|
AVAILABLE HELPERS:
|
|
17398
17521
|
- page: Playwright Page object (https://playwright.dev/docs/api/class-page)
|
|
17399
|
-
- snapshot(opts?): Get ARIA tree. opts: {maxDepth, interactableOnly, format}
|
|
17522
|
+
- snapshot(opts?): Get ARIA tree with React component info. opts: {maxDepth, interactableOnly, format}
|
|
17400
17523
|
- ref(id): Get element by ref ID, chainable with all ElementHandle methods
|
|
17524
|
+
- ref(id).source(): Get React component source {filePath, lineNumber, componentName}
|
|
17525
|
+
- ref(id).props(): Get React component props (serialized)
|
|
17526
|
+
- ref(id).state(): Get React component state/hooks (serialized)
|
|
17527
|
+
- component(name, opts?): Find elements by React component name. opts: {nth: number}
|
|
17401
17528
|
- fill(id, text): Clear and fill input (works with rich text editors)
|
|
17402
17529
|
- drag({from, to, dataTransfer?}): Drag with custom MIME types
|
|
17403
17530
|
- dispatch({target, event, dataTransfer?, detail?}): Dispatch custom events
|
|
17404
17531
|
- waitFor(target): Wait for selector/ref/state. e.g. waitFor('e1'), waitFor('networkidle')
|
|
17405
17532
|
- grab: React Grab client API (activate, deactivate, toggle, isActive, copyElement, getState)
|
|
17406
17533
|
|
|
17534
|
+
REACT-SPECIFIC PATTERNS:
|
|
17535
|
+
- Get React source: return await ref('e1').source()
|
|
17536
|
+
- Get component props: return await ref('e1').props()
|
|
17537
|
+
- Get component state: return await ref('e1').state()
|
|
17538
|
+
- Find by component: const btn = await component('Button', {nth: 0})
|
|
17539
|
+
|
|
17407
17540
|
ELEMENT SCREENSHOTS (PREFERRED for visual issues):
|
|
17408
17541
|
- return await ref('e1').screenshot()
|
|
17409
|
-
- return await ref('e2').screenshot()
|
|
17410
17542
|
Use for: wrong color, broken styling, visual bugs, "how does X look", UI verification
|
|
17411
|
-
Returns image directly - no file path needed.
|
|
17412
17543
|
|
|
17413
17544
|
COMMON PATTERNS:
|
|
17414
17545
|
- Click: await ref('e1').click()
|
|
17415
17546
|
- Fill input: await fill('e1', 'hello')
|
|
17416
17547
|
- Get attribute: return await ref('e1').getAttribute('href')
|
|
17417
|
-
- Get React source: return await ref('e1').source()
|
|
17418
17548
|
- Navigate: await page.goto('https://example.com')
|
|
17419
17549
|
- Full page screenshot (rare): return await page.screenshot()
|
|
17420
17550
|
|
|
@@ -17434,13 +17564,9 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17434
17564
|
let pageOpenHandler = null;
|
|
17435
17565
|
const outputJson = createOutputJson(() => activePage, pageName);
|
|
17436
17566
|
try {
|
|
17437
|
-
const
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
activePage = await browser$1.findPageByTargetId(browser2, pageInfo.targetId);
|
|
17441
|
-
if (!activePage) {
|
|
17442
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17443
|
-
}
|
|
17567
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17568
|
+
browser2 = connection.browser;
|
|
17569
|
+
activePage = connection.page;
|
|
17444
17570
|
if (url2) {
|
|
17445
17571
|
await activePage.goto(url2, {
|
|
17446
17572
|
waitUntil: "domcontentloaded",
|
|
@@ -17460,6 +17586,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17460
17586
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17461
17587
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17462
17588
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
17589
|
+
const component = createComponentHelper(getActivePage2);
|
|
17463
17590
|
const executeFunction = new Function(
|
|
17464
17591
|
"page",
|
|
17465
17592
|
"getActivePage",
|
|
@@ -17470,6 +17597,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17470
17597
|
"dispatch",
|
|
17471
17598
|
"grab",
|
|
17472
17599
|
"waitFor",
|
|
17600
|
+
"component",
|
|
17473
17601
|
`return (async () => { ${code} })();`
|
|
17474
17602
|
);
|
|
17475
17603
|
const result = await executeFunction(
|
|
@@ -17481,7 +17609,8 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17481
17609
|
drag,
|
|
17482
17610
|
dispatch,
|
|
17483
17611
|
grab,
|
|
17484
|
-
waitFor
|
|
17612
|
+
waitFor,
|
|
17613
|
+
component
|
|
17485
17614
|
);
|
|
17486
17615
|
if (Buffer.isBuffer(result)) {
|
|
17487
17616
|
const output2 = await outputJson(true, void 0);
|
|
@@ -17518,12 +17647,82 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17518
17647
|
}
|
|
17519
17648
|
}
|
|
17520
17649
|
);
|
|
17650
|
+
server.registerTool(
|
|
17651
|
+
"browser_react_tree",
|
|
17652
|
+
{
|
|
17653
|
+
description: `Get React component tree hierarchy (separate from ARIA tree).
|
|
17654
|
+
|
|
17655
|
+
Shows the React component structure with:
|
|
17656
|
+
- Component names and nesting
|
|
17657
|
+
- Source file locations
|
|
17658
|
+
- Element refs where available
|
|
17659
|
+
- Optional props (serialized)
|
|
17660
|
+
|
|
17661
|
+
Use this when you need to understand React component architecture rather than accessibility tree.
|
|
17662
|
+
For interacting with elements, use browser_snapshot to get refs first.`,
|
|
17663
|
+
inputSchema: {
|
|
17664
|
+
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17665
|
+
maxDepth: external_exports.number().optional().default(50).describe("Maximum tree depth"),
|
|
17666
|
+
includeProps: external_exports.boolean().optional().default(false).describe("Include component props (increases output size)")
|
|
17667
|
+
}
|
|
17668
|
+
},
|
|
17669
|
+
async ({ page: pageName, maxDepth, includeProps }) => {
|
|
17670
|
+
let browser2 = null;
|
|
17671
|
+
try {
|
|
17672
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17673
|
+
browser2 = connection.browser;
|
|
17674
|
+
const activePage = connection.page;
|
|
17675
|
+
const componentTree = await activePage.evaluate(
|
|
17676
|
+
async (opts2) => {
|
|
17677
|
+
const g2 = globalThis;
|
|
17678
|
+
if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
|
|
17679
|
+
return [];
|
|
17680
|
+
}
|
|
17681
|
+
return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
|
|
17682
|
+
},
|
|
17683
|
+
{ maxDepth: maxDepth ?? 50, includeProps: includeProps ?? false }
|
|
17684
|
+
);
|
|
17685
|
+
const renderTree = (nodes) => {
|
|
17686
|
+
const lines = [];
|
|
17687
|
+
for (const node of nodes) {
|
|
17688
|
+
const indent = " ".repeat(node.depth);
|
|
17689
|
+
let line = `${indent}- ${node.name}`;
|
|
17690
|
+
if (node.ref) line += ` [ref=${node.ref}]`;
|
|
17691
|
+
if (node.source) line += ` [source=${node.source}]`;
|
|
17692
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
17693
|
+
const propsStr = JSON.stringify(node.props);
|
|
17694
|
+
if (propsStr.length < 100) {
|
|
17695
|
+
line += ` [props=${propsStr}]`;
|
|
17696
|
+
} else {
|
|
17697
|
+
line += ` [props=...]`;
|
|
17698
|
+
}
|
|
17699
|
+
}
|
|
17700
|
+
lines.push(line);
|
|
17701
|
+
}
|
|
17702
|
+
return lines.join("\n");
|
|
17703
|
+
};
|
|
17704
|
+
const treeOutput = renderTree(componentTree);
|
|
17705
|
+
return {
|
|
17706
|
+
content: [
|
|
17707
|
+
{
|
|
17708
|
+
type: "text",
|
|
17709
|
+
text: treeOutput || "No React components found. Make sure react-grab is installed and the page uses React."
|
|
17710
|
+
}
|
|
17711
|
+
]
|
|
17712
|
+
};
|
|
17713
|
+
} catch (error48) {
|
|
17714
|
+
return createMcpErrorResponse(error48);
|
|
17715
|
+
} finally {
|
|
17716
|
+
await browser2?.close();
|
|
17717
|
+
}
|
|
17718
|
+
}
|
|
17719
|
+
);
|
|
17521
17720
|
const transport = new stdio_js.StdioServerTransport();
|
|
17522
17721
|
await server.connect(transport);
|
|
17523
17722
|
};
|
|
17524
17723
|
|
|
17525
17724
|
// src/commands/browser.ts
|
|
17526
|
-
var VERSION2 = "0.1.0";
|
|
17725
|
+
var VERSION2 = "0.1.0-beta.3";
|
|
17527
17726
|
var printHeader = () => {
|
|
17528
17727
|
console.log(
|
|
17529
17728
|
`${pc__default.default.magenta("\u273F")} ${pc__default.default.bold("React Grab")} ${pc__default.default.gray(VERSION2)}`
|
|
@@ -18011,14 +18210,7 @@ browser.addCommand(status);
|
|
|
18011
18210
|
browser.addCommand(execute);
|
|
18012
18211
|
browser.addCommand(pages);
|
|
18013
18212
|
browser.addCommand(mcp);
|
|
18014
|
-
|
|
18015
|
-
// src/utils/constants.ts
|
|
18016
|
-
var MAX_SUGGESTIONS_COUNT = 30;
|
|
18017
|
-
var MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
18018
|
-
var MAX_CONTEXT_LINES = 50;
|
|
18019
|
-
|
|
18020
|
-
// src/commands/configure.ts
|
|
18021
|
-
var VERSION3 = "0.1.0";
|
|
18213
|
+
var VERSION3 = "0.1.0-beta.3";
|
|
18022
18214
|
var isMac = process.platform === "darwin";
|
|
18023
18215
|
var META_LABEL = isMac ? "Cmd" : "Win";
|
|
18024
18216
|
var ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -18505,7 +18697,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
|
|
|
18505
18697
|
handleError(error48);
|
|
18506
18698
|
}
|
|
18507
18699
|
};
|
|
18508
|
-
var VERSION4 = "0.1.0";
|
|
18700
|
+
var VERSION4 = "0.1.0-beta.3";
|
|
18509
18701
|
var REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
18510
18702
|
var DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
18511
18703
|
var promptAgentIntegration = async (cwd, customPkg) => {
|
|
@@ -19197,7 +19389,7 @@ var init = new commander.Command().name("init").description("initialize React Gr
|
|
|
19197
19389
|
await reportToCli("error", void 0, error48);
|
|
19198
19390
|
}
|
|
19199
19391
|
});
|
|
19200
|
-
var VERSION5 = "0.1.0";
|
|
19392
|
+
var VERSION5 = "0.1.0-beta.3";
|
|
19201
19393
|
var remove = new commander.Command().name("remove").description("remove an agent integration").argument(
|
|
19202
19394
|
"[agent]",
|
|
19203
19395
|
"agent to remove (claude-code, cursor, opencode, codex, gemini, amp, ami, visual-edit)"
|
|
@@ -19376,7 +19568,7 @@ var remove = new commander.Command().name("remove").description("remove an agent
|
|
|
19376
19568
|
});
|
|
19377
19569
|
|
|
19378
19570
|
// src/cli.ts
|
|
19379
|
-
var VERSION6 = "0.1.0";
|
|
19571
|
+
var VERSION6 = "0.1.0-beta.3";
|
|
19380
19572
|
var VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
19381
19573
|
process.on("SIGINT", () => process.exit(0));
|
|
19382
19574
|
process.on("SIGTERM", () => process.exit(0));
|
package/dist/cli.js
CHANGED
|
@@ -2697,7 +2697,7 @@ var previewPackageJsonAgentRemoval = (projectRoot, agent) => {
|
|
|
2697
2697
|
};
|
|
2698
2698
|
|
|
2699
2699
|
// src/commands/add.ts
|
|
2700
|
-
var VERSION = "0.1.0";
|
|
2700
|
+
var VERSION = "0.1.0-beta.3";
|
|
2701
2701
|
var add = new Command().name("add").alias("install").description("add an agent integration or MCP server").argument(
|
|
2702
2702
|
"[agent]",
|
|
2703
2703
|
`agent to add (${AGENTS.join(", ")}, mcp, skill)`
|
|
@@ -3226,6 +3226,15 @@ var add = new Command().name("add").alias("install").description("add an agent i
|
|
|
3226
3226
|
handleError(error48);
|
|
3227
3227
|
}
|
|
3228
3228
|
});
|
|
3229
|
+
|
|
3230
|
+
// src/utils/constants.ts
|
|
3231
|
+
var MAX_SUGGESTIONS_COUNT = 30;
|
|
3232
|
+
var MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
3233
|
+
var MAX_CONTEXT_LINES = 50;
|
|
3234
|
+
var COMPONENT_STACK_MAX_DEPTH = 10;
|
|
3235
|
+
|
|
3236
|
+
// src/utils/browser-automation.ts
|
|
3237
|
+
var LOAD_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
|
|
3229
3238
|
var ensureHealthyServer = async (options2 = {}) => {
|
|
3230
3239
|
const cliPath = process.argv[1];
|
|
3231
3240
|
const serverRunning = await isServerRunning();
|
|
@@ -3256,16 +3265,65 @@ var getOrCreatePage = async (serverUrl, name) => {
|
|
|
3256
3265
|
}
|
|
3257
3266
|
return response.json();
|
|
3258
3267
|
};
|
|
3268
|
+
var getReactContextForActiveElement = async (page) => {
|
|
3269
|
+
try {
|
|
3270
|
+
return page.evaluate(async (maxDepth) => {
|
|
3271
|
+
const activeElement = document.activeElement;
|
|
3272
|
+
if (!activeElement || activeElement === document.body) return null;
|
|
3273
|
+
const reactGrab = globalThis.__REACT_GRAB__;
|
|
3274
|
+
if (!reactGrab?.getSource) return null;
|
|
3275
|
+
const source = await reactGrab.getSource(activeElement);
|
|
3276
|
+
if (!source) return null;
|
|
3277
|
+
const componentStack = [];
|
|
3278
|
+
if (source.componentName) {
|
|
3279
|
+
componentStack.push(source.componentName);
|
|
3280
|
+
}
|
|
3281
|
+
const fiberKey = Object.keys(activeElement).find(
|
|
3282
|
+
(key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
|
|
3283
|
+
);
|
|
3284
|
+
if (fiberKey) {
|
|
3285
|
+
let fiber = activeElement[fiberKey];
|
|
3286
|
+
let depth = 0;
|
|
3287
|
+
while (fiber?.return && depth < maxDepth) {
|
|
3288
|
+
fiber = fiber.return;
|
|
3289
|
+
if (fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 11) {
|
|
3290
|
+
const name = typeof fiber.type === "object" ? fiber.type?.displayName || fiber.type?.name : null;
|
|
3291
|
+
if (name && !name.startsWith("_") && !componentStack.includes(name)) {
|
|
3292
|
+
componentStack.push(name);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
depth++;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return {
|
|
3299
|
+
element: activeElement.tagName.toLowerCase(),
|
|
3300
|
+
component: source.componentName || void 0,
|
|
3301
|
+
source: source.filePath ? `${source.filePath}${source.lineNumber ? `:${source.lineNumber}` : ""}` : void 0,
|
|
3302
|
+
componentStack: componentStack.length > 0 ? componentStack : void 0
|
|
3303
|
+
};
|
|
3304
|
+
}, COMPONENT_STACK_MAX_DEPTH);
|
|
3305
|
+
} catch {
|
|
3306
|
+
return null;
|
|
3307
|
+
}
|
|
3308
|
+
};
|
|
3259
3309
|
var createOutputJson = (getPage, pageName) => {
|
|
3260
3310
|
return async (ok, result, error48) => {
|
|
3261
3311
|
const page = getPage();
|
|
3312
|
+
let reactContext;
|
|
3313
|
+
if (page && ok) {
|
|
3314
|
+
const context = await getReactContextForActiveElement(page);
|
|
3315
|
+
if (context) {
|
|
3316
|
+
reactContext = context;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3262
3319
|
return {
|
|
3263
3320
|
ok,
|
|
3264
3321
|
url: page?.url() ?? "",
|
|
3265
3322
|
title: page ? await page.title().catch(() => "") : "",
|
|
3266
3323
|
page: pageName,
|
|
3267
3324
|
...result !== void 0 && { result },
|
|
3268
|
-
...error48 && { error: error48 }
|
|
3325
|
+
...error48 && { error: error48 },
|
|
3326
|
+
...reactContext && { reactContext }
|
|
3269
3327
|
};
|
|
3270
3328
|
};
|
|
3271
3329
|
};
|
|
@@ -3324,6 +3382,24 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3324
3382
|
return g2.__REACT_GRAB__.getSource(el);
|
|
3325
3383
|
}, element);
|
|
3326
3384
|
};
|
|
3385
|
+
const getProps = async (refId) => {
|
|
3386
|
+
const element = await getElement(refId);
|
|
3387
|
+
const currentPage2 = getActivePage2();
|
|
3388
|
+
return currentPage2.evaluate((el) => {
|
|
3389
|
+
const g2 = globalThis;
|
|
3390
|
+
if (!g2.__REACT_GRAB_GET_PROPS__) return null;
|
|
3391
|
+
return g2.__REACT_GRAB_GET_PROPS__(el);
|
|
3392
|
+
}, element);
|
|
3393
|
+
};
|
|
3394
|
+
const getState = async (refId) => {
|
|
3395
|
+
const element = await getElement(refId);
|
|
3396
|
+
const currentPage2 = getActivePage2();
|
|
3397
|
+
return currentPage2.evaluate((el) => {
|
|
3398
|
+
const g2 = globalThis;
|
|
3399
|
+
if (!g2.__REACT_GRAB_GET_STATE__) return null;
|
|
3400
|
+
return g2.__REACT_GRAB_GET_STATE__(el);
|
|
3401
|
+
}, element);
|
|
3402
|
+
};
|
|
3327
3403
|
return (refId) => {
|
|
3328
3404
|
return new Proxy(
|
|
3329
3405
|
{},
|
|
@@ -3335,6 +3411,12 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3335
3411
|
if (prop === "source") {
|
|
3336
3412
|
return () => getSource(refId);
|
|
3337
3413
|
}
|
|
3414
|
+
if (prop === "props") {
|
|
3415
|
+
return () => getProps(refId);
|
|
3416
|
+
}
|
|
3417
|
+
if (prop === "state") {
|
|
3418
|
+
return () => getState(refId);
|
|
3419
|
+
}
|
|
3338
3420
|
if (prop === "screenshot") {
|
|
3339
3421
|
return (options2) => getElement(refId).then(
|
|
3340
3422
|
(el) => el.screenshot({ scale: "css", ...options2 })
|
|
@@ -3348,6 +3430,50 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3348
3430
|
);
|
|
3349
3431
|
};
|
|
3350
3432
|
};
|
|
3433
|
+
var createComponentHelper = (getActivePage2) => {
|
|
3434
|
+
return async (componentName, options2) => {
|
|
3435
|
+
const currentPage2 = getActivePage2();
|
|
3436
|
+
const nth = options2?.nth;
|
|
3437
|
+
const elementHandles = await currentPage2.evaluateHandle(
|
|
3438
|
+
async (args) => {
|
|
3439
|
+
const g2 = globalThis;
|
|
3440
|
+
if (!g2.__REACT_GRAB_FIND_BY_COMPONENT__) {
|
|
3441
|
+
throw new Error("React introspection not available. Make sure react-grab is installed.");
|
|
3442
|
+
}
|
|
3443
|
+
const result = await g2.__REACT_GRAB_FIND_BY_COMPONENT__(args.name, args.nth !== void 0 ? { nth: args.nth } : void 0);
|
|
3444
|
+
if (!result) return null;
|
|
3445
|
+
if (args.nth !== void 0) {
|
|
3446
|
+
const single = result;
|
|
3447
|
+
return single?.element || null;
|
|
3448
|
+
}
|
|
3449
|
+
const arr = result;
|
|
3450
|
+
return arr.map((m2) => m2.element);
|
|
3451
|
+
},
|
|
3452
|
+
{ name: componentName, nth }
|
|
3453
|
+
);
|
|
3454
|
+
const value = await elementHandles.jsonValue().catch(() => null);
|
|
3455
|
+
if (value === null) {
|
|
3456
|
+
await elementHandles.dispose();
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
if (nth !== void 0) {
|
|
3460
|
+
const element = elementHandles.asElement();
|
|
3461
|
+
if (!element) {
|
|
3462
|
+
await elementHandles.dispose();
|
|
3463
|
+
return null;
|
|
3464
|
+
}
|
|
3465
|
+
return element;
|
|
3466
|
+
}
|
|
3467
|
+
const jsHandles = await elementHandles.getProperties();
|
|
3468
|
+
const handles = [];
|
|
3469
|
+
for (const [, handle] of jsHandles) {
|
|
3470
|
+
const element = handle.asElement();
|
|
3471
|
+
if (element) handles.push(element);
|
|
3472
|
+
}
|
|
3473
|
+
await elementHandles.dispose();
|
|
3474
|
+
return handles;
|
|
3475
|
+
};
|
|
3476
|
+
};
|
|
3351
3477
|
var createFillHelper = (ref, getActivePage2) => {
|
|
3352
3478
|
return async (refId, text) => {
|
|
3353
3479
|
const element = await ref(refId);
|
|
@@ -3449,35 +3575,22 @@ var createDispatchHelper = (getActivePage2) => {
|
|
|
3449
3575
|
};
|
|
3450
3576
|
};
|
|
3451
3577
|
var createGrabHelper = (ref, getActivePage2) => {
|
|
3578
|
+
const evaluateGrabMethod = async (methodName, defaultValue) => {
|
|
3579
|
+
const currentPage2 = getActivePage2();
|
|
3580
|
+
return currentPage2.evaluate(
|
|
3581
|
+
({ method, fallback }) => {
|
|
3582
|
+
const grab = globalThis.__REACT_GRAB__;
|
|
3583
|
+
return grab?.[method]?.() ?? fallback;
|
|
3584
|
+
},
|
|
3585
|
+
{ method: methodName, fallback: defaultValue }
|
|
3586
|
+
);
|
|
3587
|
+
};
|
|
3452
3588
|
return {
|
|
3453
|
-
activate:
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
});
|
|
3459
|
-
},
|
|
3460
|
-
deactivate: async () => {
|
|
3461
|
-
const currentPage2 = getActivePage2();
|
|
3462
|
-
await currentPage2.evaluate(() => {
|
|
3463
|
-
const g2 = globalThis;
|
|
3464
|
-
g2.__REACT_GRAB__?.deactivate();
|
|
3465
|
-
});
|
|
3466
|
-
},
|
|
3467
|
-
toggle: async () => {
|
|
3468
|
-
const currentPage2 = getActivePage2();
|
|
3469
|
-
await currentPage2.evaluate(() => {
|
|
3470
|
-
const g2 = globalThis;
|
|
3471
|
-
g2.__REACT_GRAB__?.toggle();
|
|
3472
|
-
});
|
|
3473
|
-
},
|
|
3474
|
-
isActive: async () => {
|
|
3475
|
-
const currentPage2 = getActivePage2();
|
|
3476
|
-
return currentPage2.evaluate(() => {
|
|
3477
|
-
const g2 = globalThis;
|
|
3478
|
-
return g2.__REACT_GRAB__?.isActive() ?? false;
|
|
3479
|
-
});
|
|
3480
|
-
},
|
|
3589
|
+
activate: () => evaluateGrabMethod("activate", void 0),
|
|
3590
|
+
deactivate: () => evaluateGrabMethod("deactivate", void 0),
|
|
3591
|
+
toggle: () => evaluateGrabMethod("toggle", void 0),
|
|
3592
|
+
isActive: () => evaluateGrabMethod("isActive", false),
|
|
3593
|
+
getState: () => evaluateGrabMethod("getState", null),
|
|
3481
3594
|
copyElement: async (refId) => {
|
|
3482
3595
|
const element = await ref(refId);
|
|
3483
3596
|
if (!element) return false;
|
|
@@ -3486,13 +3599,6 @@ var createGrabHelper = (ref, getActivePage2) => {
|
|
|
3486
3599
|
const g2 = globalThis;
|
|
3487
3600
|
return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
|
|
3488
3601
|
}, element);
|
|
3489
|
-
},
|
|
3490
|
-
getState: async () => {
|
|
3491
|
-
const currentPage2 = getActivePage2();
|
|
3492
|
-
return currentPage2.evaluate(() => {
|
|
3493
|
-
const g2 = globalThis;
|
|
3494
|
-
return g2.__REACT_GRAB__?.getState() ?? null;
|
|
3495
|
-
});
|
|
3496
3602
|
}
|
|
3497
3603
|
};
|
|
3498
3604
|
};
|
|
@@ -3508,7 +3614,7 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3508
3614
|
return async (selectorOrState, options2) => {
|
|
3509
3615
|
const currentPage2 = getActivePage2();
|
|
3510
3616
|
const timeout = options2?.timeout;
|
|
3511
|
-
if (selectorOrState
|
|
3617
|
+
if (LOAD_STATES.has(selectorOrState)) {
|
|
3512
3618
|
await currentPage2.waitForLoadState(selectorOrState, { timeout });
|
|
3513
3619
|
return;
|
|
3514
3620
|
}
|
|
@@ -3519,6 +3625,33 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3519
3625
|
await currentPage2.waitForSelector(selectorOrState, { timeout });
|
|
3520
3626
|
};
|
|
3521
3627
|
};
|
|
3628
|
+
var connectToBrowserPage = async (pageName) => {
|
|
3629
|
+
const { chromium: chromium2 } = await import('playwright-core');
|
|
3630
|
+
const { findPageByTargetId: findPageByTargetId2 } = await import('@react-grab/browser');
|
|
3631
|
+
const { serverUrl } = await ensureHealthyServer();
|
|
3632
|
+
const pageInfo = await getOrCreatePage(serverUrl, pageName);
|
|
3633
|
+
const browser2 = await chromium2.connectOverCDP(pageInfo.wsEndpoint);
|
|
3634
|
+
const page = await findPageByTargetId2(browser2, pageInfo.targetId);
|
|
3635
|
+
if (!page) {
|
|
3636
|
+
await browser2.close();
|
|
3637
|
+
throw new Error(`Page "${pageName}" not found`);
|
|
3638
|
+
}
|
|
3639
|
+
return { browser: browser2, page, serverUrl };
|
|
3640
|
+
};
|
|
3641
|
+
var createMcpErrorResponse = (error48) => {
|
|
3642
|
+
return {
|
|
3643
|
+
content: [
|
|
3644
|
+
{
|
|
3645
|
+
type: "text",
|
|
3646
|
+
text: JSON.stringify({
|
|
3647
|
+
ok: false,
|
|
3648
|
+
error: error48 instanceof Error ? error48.message : "Failed"
|
|
3649
|
+
})
|
|
3650
|
+
}
|
|
3651
|
+
],
|
|
3652
|
+
isError: true
|
|
3653
|
+
};
|
|
3654
|
+
};
|
|
3522
3655
|
|
|
3523
3656
|
// ../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/external.js
|
|
3524
3657
|
var external_exports = {};
|
|
@@ -17288,7 +17421,13 @@ var startMcpServer = async () => {
|
|
|
17288
17421
|
server.registerTool(
|
|
17289
17422
|
"browser_snapshot",
|
|
17290
17423
|
{
|
|
17291
|
-
description: `Get ARIA accessibility tree with element refs (e1, e2...).
|
|
17424
|
+
description: `Get ARIA accessibility tree with element refs (e1, e2...) and React component info.
|
|
17425
|
+
|
|
17426
|
+
OUTPUT INCLUDES:
|
|
17427
|
+
- ARIA roles and accessible names
|
|
17428
|
+
- Element refs (e1, e2...) for interaction
|
|
17429
|
+
- [component=ComponentName] for React components
|
|
17430
|
+
- [source=file.tsx:line] for source location
|
|
17292
17431
|
|
|
17293
17432
|
SCREENSHOT STRATEGY - ALWAYS prefer element screenshots over full page:
|
|
17294
17433
|
1. First: Get refs with snapshot (this tool)
|
|
@@ -17306,7 +17445,7 @@ USE VIEWPORT screenshot=true ONLY FOR:
|
|
|
17306
17445
|
|
|
17307
17446
|
PERFORMANCE:
|
|
17308
17447
|
- interactableOnly:true = much smaller output (recommended)
|
|
17309
|
-
- format:'compact' = minimal ref:role:name output
|
|
17448
|
+
- format:'compact' = minimal ref:role:name@Component output
|
|
17310
17449
|
- maxDepth = limit tree depth
|
|
17311
17450
|
|
|
17312
17451
|
After getting refs, use browser_execute with: ref('e1').click()`,
|
|
@@ -17327,16 +17466,11 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17327
17466
|
format,
|
|
17328
17467
|
screenshot
|
|
17329
17468
|
}) => {
|
|
17330
|
-
let activePage = null;
|
|
17331
17469
|
let browser2 = null;
|
|
17332
17470
|
try {
|
|
17333
|
-
const
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
activePage = await findPageByTargetId(browser2, pageInfo.targetId);
|
|
17337
|
-
if (!activePage) {
|
|
17338
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17339
|
-
}
|
|
17471
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17472
|
+
browser2 = connection.browser;
|
|
17473
|
+
const activePage = connection.page;
|
|
17340
17474
|
const getActivePage2 = () => activePage;
|
|
17341
17475
|
const snapshot = createSnapshotHelper(getActivePage2);
|
|
17342
17476
|
const snapshotResult = await snapshot({ maxDepth, interactableOnly, format });
|
|
@@ -17360,18 +17494,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17360
17494
|
content: [{ type: "text", text: snapshotResult }]
|
|
17361
17495
|
};
|
|
17362
17496
|
} catch (error48) {
|
|
17363
|
-
return
|
|
17364
|
-
content: [
|
|
17365
|
-
{
|
|
17366
|
-
type: "text",
|
|
17367
|
-
text: JSON.stringify({
|
|
17368
|
-
ok: false,
|
|
17369
|
-
error: error48 instanceof Error ? error48.message : "Failed"
|
|
17370
|
-
})
|
|
17371
|
-
}
|
|
17372
|
-
],
|
|
17373
|
-
isError: true
|
|
17374
|
-
};
|
|
17497
|
+
return createMcpErrorResponse(error48);
|
|
17375
17498
|
} finally {
|
|
17376
17499
|
await browser2?.close();
|
|
17377
17500
|
}
|
|
@@ -17386,25 +17509,32 @@ IMPORTANT: Always call snapshot() first to get element refs from the a11y tree (
|
|
|
17386
17509
|
|
|
17387
17510
|
AVAILABLE HELPERS:
|
|
17388
17511
|
- page: Playwright Page object (https://playwright.dev/docs/api/class-page)
|
|
17389
|
-
- snapshot(opts?): Get ARIA tree. opts: {maxDepth, interactableOnly, format}
|
|
17512
|
+
- snapshot(opts?): Get ARIA tree with React component info. opts: {maxDepth, interactableOnly, format}
|
|
17390
17513
|
- ref(id): Get element by ref ID, chainable with all ElementHandle methods
|
|
17514
|
+
- ref(id).source(): Get React component source {filePath, lineNumber, componentName}
|
|
17515
|
+
- ref(id).props(): Get React component props (serialized)
|
|
17516
|
+
- ref(id).state(): Get React component state/hooks (serialized)
|
|
17517
|
+
- component(name, opts?): Find elements by React component name. opts: {nth: number}
|
|
17391
17518
|
- fill(id, text): Clear and fill input (works with rich text editors)
|
|
17392
17519
|
- drag({from, to, dataTransfer?}): Drag with custom MIME types
|
|
17393
17520
|
- dispatch({target, event, dataTransfer?, detail?}): Dispatch custom events
|
|
17394
17521
|
- waitFor(target): Wait for selector/ref/state. e.g. waitFor('e1'), waitFor('networkidle')
|
|
17395
17522
|
- grab: React Grab client API (activate, deactivate, toggle, isActive, copyElement, getState)
|
|
17396
17523
|
|
|
17524
|
+
REACT-SPECIFIC PATTERNS:
|
|
17525
|
+
- Get React source: return await ref('e1').source()
|
|
17526
|
+
- Get component props: return await ref('e1').props()
|
|
17527
|
+
- Get component state: return await ref('e1').state()
|
|
17528
|
+
- Find by component: const btn = await component('Button', {nth: 0})
|
|
17529
|
+
|
|
17397
17530
|
ELEMENT SCREENSHOTS (PREFERRED for visual issues):
|
|
17398
17531
|
- return await ref('e1').screenshot()
|
|
17399
|
-
- return await ref('e2').screenshot()
|
|
17400
17532
|
Use for: wrong color, broken styling, visual bugs, "how does X look", UI verification
|
|
17401
|
-
Returns image directly - no file path needed.
|
|
17402
17533
|
|
|
17403
17534
|
COMMON PATTERNS:
|
|
17404
17535
|
- Click: await ref('e1').click()
|
|
17405
17536
|
- Fill input: await fill('e1', 'hello')
|
|
17406
17537
|
- Get attribute: return await ref('e1').getAttribute('href')
|
|
17407
|
-
- Get React source: return await ref('e1').source()
|
|
17408
17538
|
- Navigate: await page.goto('https://example.com')
|
|
17409
17539
|
- Full page screenshot (rare): return await page.screenshot()
|
|
17410
17540
|
|
|
@@ -17424,13 +17554,9 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17424
17554
|
let pageOpenHandler = null;
|
|
17425
17555
|
const outputJson = createOutputJson(() => activePage, pageName);
|
|
17426
17556
|
try {
|
|
17427
|
-
const
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
activePage = await findPageByTargetId(browser2, pageInfo.targetId);
|
|
17431
|
-
if (!activePage) {
|
|
17432
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17433
|
-
}
|
|
17557
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17558
|
+
browser2 = connection.browser;
|
|
17559
|
+
activePage = connection.page;
|
|
17434
17560
|
if (url2) {
|
|
17435
17561
|
await activePage.goto(url2, {
|
|
17436
17562
|
waitUntil: "domcontentloaded",
|
|
@@ -17450,6 +17576,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17450
17576
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17451
17577
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17452
17578
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
17579
|
+
const component = createComponentHelper(getActivePage2);
|
|
17453
17580
|
const executeFunction = new Function(
|
|
17454
17581
|
"page",
|
|
17455
17582
|
"getActivePage",
|
|
@@ -17460,6 +17587,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17460
17587
|
"dispatch",
|
|
17461
17588
|
"grab",
|
|
17462
17589
|
"waitFor",
|
|
17590
|
+
"component",
|
|
17463
17591
|
`return (async () => { ${code} })();`
|
|
17464
17592
|
);
|
|
17465
17593
|
const result = await executeFunction(
|
|
@@ -17471,7 +17599,8 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17471
17599
|
drag,
|
|
17472
17600
|
dispatch,
|
|
17473
17601
|
grab,
|
|
17474
|
-
waitFor
|
|
17602
|
+
waitFor,
|
|
17603
|
+
component
|
|
17475
17604
|
);
|
|
17476
17605
|
if (Buffer.isBuffer(result)) {
|
|
17477
17606
|
const output2 = await outputJson(true, void 0);
|
|
@@ -17508,12 +17637,82 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17508
17637
|
}
|
|
17509
17638
|
}
|
|
17510
17639
|
);
|
|
17640
|
+
server.registerTool(
|
|
17641
|
+
"browser_react_tree",
|
|
17642
|
+
{
|
|
17643
|
+
description: `Get React component tree hierarchy (separate from ARIA tree).
|
|
17644
|
+
|
|
17645
|
+
Shows the React component structure with:
|
|
17646
|
+
- Component names and nesting
|
|
17647
|
+
- Source file locations
|
|
17648
|
+
- Element refs where available
|
|
17649
|
+
- Optional props (serialized)
|
|
17650
|
+
|
|
17651
|
+
Use this when you need to understand React component architecture rather than accessibility tree.
|
|
17652
|
+
For interacting with elements, use browser_snapshot to get refs first.`,
|
|
17653
|
+
inputSchema: {
|
|
17654
|
+
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17655
|
+
maxDepth: external_exports.number().optional().default(50).describe("Maximum tree depth"),
|
|
17656
|
+
includeProps: external_exports.boolean().optional().default(false).describe("Include component props (increases output size)")
|
|
17657
|
+
}
|
|
17658
|
+
},
|
|
17659
|
+
async ({ page: pageName, maxDepth, includeProps }) => {
|
|
17660
|
+
let browser2 = null;
|
|
17661
|
+
try {
|
|
17662
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17663
|
+
browser2 = connection.browser;
|
|
17664
|
+
const activePage = connection.page;
|
|
17665
|
+
const componentTree = await activePage.evaluate(
|
|
17666
|
+
async (opts2) => {
|
|
17667
|
+
const g2 = globalThis;
|
|
17668
|
+
if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
|
|
17669
|
+
return [];
|
|
17670
|
+
}
|
|
17671
|
+
return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
|
|
17672
|
+
},
|
|
17673
|
+
{ maxDepth: maxDepth ?? 50, includeProps: includeProps ?? false }
|
|
17674
|
+
);
|
|
17675
|
+
const renderTree = (nodes) => {
|
|
17676
|
+
const lines = [];
|
|
17677
|
+
for (const node of nodes) {
|
|
17678
|
+
const indent = " ".repeat(node.depth);
|
|
17679
|
+
let line = `${indent}- ${node.name}`;
|
|
17680
|
+
if (node.ref) line += ` [ref=${node.ref}]`;
|
|
17681
|
+
if (node.source) line += ` [source=${node.source}]`;
|
|
17682
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
17683
|
+
const propsStr = JSON.stringify(node.props);
|
|
17684
|
+
if (propsStr.length < 100) {
|
|
17685
|
+
line += ` [props=${propsStr}]`;
|
|
17686
|
+
} else {
|
|
17687
|
+
line += ` [props=...]`;
|
|
17688
|
+
}
|
|
17689
|
+
}
|
|
17690
|
+
lines.push(line);
|
|
17691
|
+
}
|
|
17692
|
+
return lines.join("\n");
|
|
17693
|
+
};
|
|
17694
|
+
const treeOutput = renderTree(componentTree);
|
|
17695
|
+
return {
|
|
17696
|
+
content: [
|
|
17697
|
+
{
|
|
17698
|
+
type: "text",
|
|
17699
|
+
text: treeOutput || "No React components found. Make sure react-grab is installed and the page uses React."
|
|
17700
|
+
}
|
|
17701
|
+
]
|
|
17702
|
+
};
|
|
17703
|
+
} catch (error48) {
|
|
17704
|
+
return createMcpErrorResponse(error48);
|
|
17705
|
+
} finally {
|
|
17706
|
+
await browser2?.close();
|
|
17707
|
+
}
|
|
17708
|
+
}
|
|
17709
|
+
);
|
|
17511
17710
|
const transport = new StdioServerTransport();
|
|
17512
17711
|
await server.connect(transport);
|
|
17513
17712
|
};
|
|
17514
17713
|
|
|
17515
17714
|
// src/commands/browser.ts
|
|
17516
|
-
var VERSION2 = "0.1.0";
|
|
17715
|
+
var VERSION2 = "0.1.0-beta.3";
|
|
17517
17716
|
var printHeader = () => {
|
|
17518
17717
|
console.log(
|
|
17519
17718
|
`${pc.magenta("\u273F")} ${pc.bold("React Grab")} ${pc.gray(VERSION2)}`
|
|
@@ -18001,14 +18200,7 @@ browser.addCommand(status);
|
|
|
18001
18200
|
browser.addCommand(execute);
|
|
18002
18201
|
browser.addCommand(pages);
|
|
18003
18202
|
browser.addCommand(mcp);
|
|
18004
|
-
|
|
18005
|
-
// src/utils/constants.ts
|
|
18006
|
-
var MAX_SUGGESTIONS_COUNT = 30;
|
|
18007
|
-
var MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
18008
|
-
var MAX_CONTEXT_LINES = 50;
|
|
18009
|
-
|
|
18010
|
-
// src/commands/configure.ts
|
|
18011
|
-
var VERSION3 = "0.1.0";
|
|
18203
|
+
var VERSION3 = "0.1.0-beta.3";
|
|
18012
18204
|
var isMac = process.platform === "darwin";
|
|
18013
18205
|
var META_LABEL = isMac ? "Cmd" : "Win";
|
|
18014
18206
|
var ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -18495,7 +18687,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
|
|
|
18495
18687
|
handleError(error48);
|
|
18496
18688
|
}
|
|
18497
18689
|
};
|
|
18498
|
-
var VERSION4 = "0.1.0";
|
|
18690
|
+
var VERSION4 = "0.1.0-beta.3";
|
|
18499
18691
|
var REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
18500
18692
|
var DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
18501
18693
|
var promptAgentIntegration = async (cwd, customPkg) => {
|
|
@@ -19187,7 +19379,7 @@ var init = new Command().name("init").description("initialize React Grab in your
|
|
|
19187
19379
|
await reportToCli("error", void 0, error48);
|
|
19188
19380
|
}
|
|
19189
19381
|
});
|
|
19190
|
-
var VERSION5 = "0.1.0";
|
|
19382
|
+
var VERSION5 = "0.1.0-beta.3";
|
|
19191
19383
|
var remove = new Command().name("remove").description("remove an agent integration").argument(
|
|
19192
19384
|
"[agent]",
|
|
19193
19385
|
"agent to remove (claude-code, cursor, opencode, codex, gemini, amp, ami, visual-edit)"
|
|
@@ -19366,7 +19558,7 @@ var remove = new Command().name("remove").description("remove an agent integrati
|
|
|
19366
19558
|
});
|
|
19367
19559
|
|
|
19368
19560
|
// src/cli.ts
|
|
19369
|
-
var VERSION6 = "0.1.0";
|
|
19561
|
+
var VERSION6 = "0.1.0-beta.3";
|
|
19370
19562
|
var VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
19371
19563
|
process.on("SIGINT", () => process.exit(0));
|
|
19372
19564
|
process.on("SIGTERM", () => process.exit(0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-grab/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"react-grab": "./dist/cli.js"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"playwright-core": "^1.50.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
34
|
"zod": "^4.3.5",
|
|
35
|
-
"@react-grab/browser": "0.1.0-beta.
|
|
35
|
+
"@react-grab/browser": "0.1.0-beta.4"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"dev": "tsup --watch",
|