@react-grab/cli 0.1.0-beta.3 → 0.1.0-beta.5
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 +381 -135
- package/dist/cli.js +381 -135
- 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.4";
|
|
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
|
};
|
|
@@ -3327,12 +3385,42 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3327
3385
|
};
|
|
3328
3386
|
const getSource = async (refId) => {
|
|
3329
3387
|
const element = await getElement(refId);
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3388
|
+
try {
|
|
3389
|
+
const currentPage2 = getActivePage2();
|
|
3390
|
+
return await currentPage2.evaluate((el) => {
|
|
3391
|
+
const g2 = globalThis;
|
|
3392
|
+
if (!g2.__REACT_GRAB__) return null;
|
|
3393
|
+
return g2.__REACT_GRAB__.getSource(el);
|
|
3394
|
+
}, element);
|
|
3395
|
+
} finally {
|
|
3396
|
+
await element.dispose();
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
const getProps = async (refId) => {
|
|
3400
|
+
const element = await getElement(refId);
|
|
3401
|
+
try {
|
|
3402
|
+
const currentPage2 = getActivePage2();
|
|
3403
|
+
return await currentPage2.evaluate((el) => {
|
|
3404
|
+
const g2 = globalThis;
|
|
3405
|
+
if (!g2.__REACT_GRAB_GET_PROPS__) return null;
|
|
3406
|
+
return g2.__REACT_GRAB_GET_PROPS__(el);
|
|
3407
|
+
}, element);
|
|
3408
|
+
} finally {
|
|
3409
|
+
await element.dispose();
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
const getState = async (refId) => {
|
|
3413
|
+
const element = await getElement(refId);
|
|
3414
|
+
try {
|
|
3415
|
+
const currentPage2 = getActivePage2();
|
|
3416
|
+
return await currentPage2.evaluate((el) => {
|
|
3417
|
+
const g2 = globalThis;
|
|
3418
|
+
if (!g2.__REACT_GRAB_GET_STATE__) return null;
|
|
3419
|
+
return g2.__REACT_GRAB_GET_STATE__(el);
|
|
3420
|
+
}, element);
|
|
3421
|
+
} finally {
|
|
3422
|
+
await element.dispose();
|
|
3423
|
+
}
|
|
3336
3424
|
};
|
|
3337
3425
|
return (refId) => {
|
|
3338
3426
|
return new Proxy(
|
|
@@ -3345,19 +3433,79 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3345
3433
|
if (prop === "source") {
|
|
3346
3434
|
return () => getSource(refId);
|
|
3347
3435
|
}
|
|
3436
|
+
if (prop === "props") {
|
|
3437
|
+
return () => getProps(refId);
|
|
3438
|
+
}
|
|
3439
|
+
if (prop === "state") {
|
|
3440
|
+
return () => getState(refId);
|
|
3441
|
+
}
|
|
3348
3442
|
if (prop === "screenshot") {
|
|
3349
|
-
return (options2) =>
|
|
3350
|
-
|
|
3351
|
-
|
|
3443
|
+
return async (options2) => {
|
|
3444
|
+
const el = await getElement(refId);
|
|
3445
|
+
try {
|
|
3446
|
+
return await el.screenshot({ scale: "css", ...options2 });
|
|
3447
|
+
} finally {
|
|
3448
|
+
await el.dispose();
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3352
3451
|
}
|
|
3353
|
-
return (...args) =>
|
|
3354
|
-
|
|
3355
|
-
|
|
3452
|
+
return async (...args) => {
|
|
3453
|
+
const el = await getElement(refId);
|
|
3454
|
+
try {
|
|
3455
|
+
return await el[prop](...args);
|
|
3456
|
+
} finally {
|
|
3457
|
+
await el.dispose();
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3356
3460
|
}
|
|
3357
3461
|
}
|
|
3358
3462
|
);
|
|
3359
3463
|
};
|
|
3360
3464
|
};
|
|
3465
|
+
var createComponentHelper = (getActivePage2) => {
|
|
3466
|
+
return async (componentName, options2) => {
|
|
3467
|
+
const currentPage2 = getActivePage2();
|
|
3468
|
+
const nth = options2?.nth;
|
|
3469
|
+
const elementHandles = await currentPage2.evaluateHandle(
|
|
3470
|
+
async (args) => {
|
|
3471
|
+
const g2 = globalThis;
|
|
3472
|
+
if (!g2.__REACT_GRAB_FIND_BY_COMPONENT__) {
|
|
3473
|
+
throw new Error("React introspection not available. Make sure react-grab is installed.");
|
|
3474
|
+
}
|
|
3475
|
+
const result = await g2.__REACT_GRAB_FIND_BY_COMPONENT__(args.name, args.nth !== void 0 ? { nth: args.nth } : void 0);
|
|
3476
|
+
if (!result) return null;
|
|
3477
|
+
if (args.nth !== void 0) {
|
|
3478
|
+
const single = result;
|
|
3479
|
+
return single?.element || null;
|
|
3480
|
+
}
|
|
3481
|
+
const arr = result;
|
|
3482
|
+
return arr.map((m2) => m2.element);
|
|
3483
|
+
},
|
|
3484
|
+
{ name: componentName, nth }
|
|
3485
|
+
);
|
|
3486
|
+
const isNull = await currentPage2.evaluate((value) => value === null, elementHandles);
|
|
3487
|
+
if (isNull) {
|
|
3488
|
+
await elementHandles.dispose();
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
if (nth !== void 0) {
|
|
3492
|
+
const element = elementHandles.asElement();
|
|
3493
|
+
if (!element) {
|
|
3494
|
+
await elementHandles.dispose();
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
return element;
|
|
3498
|
+
}
|
|
3499
|
+
const jsHandles = await elementHandles.getProperties();
|
|
3500
|
+
const handles = [];
|
|
3501
|
+
for (const [, handle] of jsHandles) {
|
|
3502
|
+
const element = handle.asElement();
|
|
3503
|
+
if (element) handles.push(element);
|
|
3504
|
+
}
|
|
3505
|
+
await elementHandles.dispose();
|
|
3506
|
+
return handles;
|
|
3507
|
+
};
|
|
3508
|
+
};
|
|
3361
3509
|
var createFillHelper = (ref, getActivePage2) => {
|
|
3362
3510
|
return async (refId, text) => {
|
|
3363
3511
|
const element = await ref(refId);
|
|
@@ -3459,50 +3607,34 @@ var createDispatchHelper = (getActivePage2) => {
|
|
|
3459
3607
|
};
|
|
3460
3608
|
};
|
|
3461
3609
|
var createGrabHelper = (ref, getActivePage2) => {
|
|
3610
|
+
const evaluateGrabMethod = async (methodName, defaultValue) => {
|
|
3611
|
+
const currentPage2 = getActivePage2();
|
|
3612
|
+
return currentPage2.evaluate(
|
|
3613
|
+
({ method, fallback }) => {
|
|
3614
|
+
const grab = globalThis.__REACT_GRAB__;
|
|
3615
|
+
return grab?.[method]?.() ?? fallback;
|
|
3616
|
+
},
|
|
3617
|
+
{ method: methodName, fallback: defaultValue }
|
|
3618
|
+
);
|
|
3619
|
+
};
|
|
3462
3620
|
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
|
-
},
|
|
3621
|
+
activate: () => evaluateGrabMethod("activate", void 0),
|
|
3622
|
+
deactivate: () => evaluateGrabMethod("deactivate", void 0),
|
|
3623
|
+
toggle: () => evaluateGrabMethod("toggle", void 0),
|
|
3624
|
+
isActive: () => evaluateGrabMethod("isActive", false),
|
|
3625
|
+
getState: () => evaluateGrabMethod("getState", null),
|
|
3491
3626
|
copyElement: async (refId) => {
|
|
3492
3627
|
const element = await ref(refId);
|
|
3493
3628
|
if (!element) return false;
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
const g2 = globalThis;
|
|
3504
|
-
return g2.__REACT_GRAB__?.getState() ?? null;
|
|
3505
|
-
});
|
|
3629
|
+
try {
|
|
3630
|
+
const currentPage2 = getActivePage2();
|
|
3631
|
+
return await currentPage2.evaluate((el) => {
|
|
3632
|
+
const g2 = globalThis;
|
|
3633
|
+
return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
|
|
3634
|
+
}, element);
|
|
3635
|
+
} finally {
|
|
3636
|
+
await element.dispose();
|
|
3637
|
+
}
|
|
3506
3638
|
}
|
|
3507
3639
|
};
|
|
3508
3640
|
};
|
|
@@ -3518,7 +3650,7 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3518
3650
|
return async (selectorOrState, options2) => {
|
|
3519
3651
|
const currentPage2 = getActivePage2();
|
|
3520
3652
|
const timeout = options2?.timeout;
|
|
3521
|
-
if (selectorOrState
|
|
3653
|
+
if (LOAD_STATES.has(selectorOrState)) {
|
|
3522
3654
|
await currentPage2.waitForLoadState(selectorOrState, { timeout });
|
|
3523
3655
|
return;
|
|
3524
3656
|
}
|
|
@@ -3529,6 +3661,33 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3529
3661
|
await currentPage2.waitForSelector(selectorOrState, { timeout });
|
|
3530
3662
|
};
|
|
3531
3663
|
};
|
|
3664
|
+
var connectToBrowserPage = async (pageName) => {
|
|
3665
|
+
const { chromium: chromium2 } = await import('playwright-core');
|
|
3666
|
+
const { findPageByTargetId: findPageByTargetId2 } = await import('@react-grab/browser');
|
|
3667
|
+
const { serverUrl } = await ensureHealthyServer();
|
|
3668
|
+
const pageInfo = await getOrCreatePage(serverUrl, pageName);
|
|
3669
|
+
const browser2 = await chromium2.connectOverCDP(pageInfo.wsEndpoint);
|
|
3670
|
+
const page = await findPageByTargetId2(browser2, pageInfo.targetId);
|
|
3671
|
+
if (!page) {
|
|
3672
|
+
await browser2.close();
|
|
3673
|
+
throw new Error(`Page "${pageName}" not found`);
|
|
3674
|
+
}
|
|
3675
|
+
return { browser: browser2, page, serverUrl };
|
|
3676
|
+
};
|
|
3677
|
+
var createMcpErrorResponse = (error48) => {
|
|
3678
|
+
return {
|
|
3679
|
+
content: [
|
|
3680
|
+
{
|
|
3681
|
+
type: "text",
|
|
3682
|
+
text: JSON.stringify({
|
|
3683
|
+
ok: false,
|
|
3684
|
+
error: error48 instanceof Error ? error48.message : "Failed"
|
|
3685
|
+
})
|
|
3686
|
+
}
|
|
3687
|
+
],
|
|
3688
|
+
isError: true
|
|
3689
|
+
};
|
|
3690
|
+
};
|
|
3532
3691
|
|
|
3533
3692
|
// ../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/external.js
|
|
3534
3693
|
var external_exports = {};
|
|
@@ -17298,7 +17457,13 @@ var startMcpServer = async () => {
|
|
|
17298
17457
|
server.registerTool(
|
|
17299
17458
|
"browser_snapshot",
|
|
17300
17459
|
{
|
|
17301
|
-
description: `Get ARIA accessibility tree with element refs (e1, e2...).
|
|
17460
|
+
description: `Get ARIA accessibility tree with element refs (e1, e2...) and React component info.
|
|
17461
|
+
|
|
17462
|
+
OUTPUT INCLUDES:
|
|
17463
|
+
- ARIA roles and accessible names
|
|
17464
|
+
- Element refs (e1, e2...) for interaction
|
|
17465
|
+
- [component=ComponentName] for React components
|
|
17466
|
+
- [source=file.tsx:line] for source location
|
|
17302
17467
|
|
|
17303
17468
|
SCREENSHOT STRATEGY - ALWAYS prefer element screenshots over full page:
|
|
17304
17469
|
1. First: Get refs with snapshot (this tool)
|
|
@@ -17316,7 +17481,6 @@ USE VIEWPORT screenshot=true ONLY FOR:
|
|
|
17316
17481
|
|
|
17317
17482
|
PERFORMANCE:
|
|
17318
17483
|
- interactableOnly:true = much smaller output (recommended)
|
|
17319
|
-
- format:'compact' = minimal ref:role:name output
|
|
17320
17484
|
- maxDepth = limit tree depth
|
|
17321
17485
|
|
|
17322
17486
|
After getting refs, use browser_execute with: ref('e1').click()`,
|
|
@@ -17324,7 +17488,6 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17324
17488
|
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17325
17489
|
maxDepth: external_exports.number().optional().describe("Limit tree depth"),
|
|
17326
17490
|
interactableOnly: external_exports.boolean().optional().describe("Only clickable/input elements (recommended)"),
|
|
17327
|
-
format: external_exports.enum(["yaml", "compact"]).optional().default("yaml").describe("'yaml' or 'compact'"),
|
|
17328
17491
|
screenshot: external_exports.boolean().optional().default(false).describe(
|
|
17329
17492
|
"Viewport screenshot. For element screenshots (PREFERRED), use browser_execute: ref('eX').screenshot()"
|
|
17330
17493
|
)
|
|
@@ -17334,22 +17497,16 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17334
17497
|
page: pageName,
|
|
17335
17498
|
maxDepth,
|
|
17336
17499
|
interactableOnly,
|
|
17337
|
-
format,
|
|
17338
17500
|
screenshot
|
|
17339
17501
|
}) => {
|
|
17340
|
-
let activePage = null;
|
|
17341
17502
|
let browser2 = null;
|
|
17342
17503
|
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
|
-
}
|
|
17504
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17505
|
+
browser2 = connection.browser;
|
|
17506
|
+
const activePage = connection.page;
|
|
17350
17507
|
const getActivePage2 = () => activePage;
|
|
17351
17508
|
const snapshot = createSnapshotHelper(getActivePage2);
|
|
17352
|
-
const snapshotResult = await snapshot({ maxDepth, interactableOnly
|
|
17509
|
+
const snapshotResult = await snapshot({ maxDepth, interactableOnly });
|
|
17353
17510
|
if (screenshot) {
|
|
17354
17511
|
const screenshotBuffer = await activePage.screenshot({
|
|
17355
17512
|
fullPage: false,
|
|
@@ -17370,18 +17527,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17370
17527
|
content: [{ type: "text", text: snapshotResult }]
|
|
17371
17528
|
};
|
|
17372
17529
|
} 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
|
-
};
|
|
17530
|
+
return createMcpErrorResponse(error48);
|
|
17385
17531
|
} finally {
|
|
17386
17532
|
await browser2?.close();
|
|
17387
17533
|
}
|
|
@@ -17396,25 +17542,32 @@ IMPORTANT: Always call snapshot() first to get element refs from the a11y tree (
|
|
|
17396
17542
|
|
|
17397
17543
|
AVAILABLE HELPERS:
|
|
17398
17544
|
- page: Playwright Page object (https://playwright.dev/docs/api/class-page)
|
|
17399
|
-
- snapshot(opts?): Get ARIA tree. opts: {maxDepth, interactableOnly
|
|
17545
|
+
- snapshot(opts?): Get ARIA tree with React component info. opts: {maxDepth, interactableOnly}
|
|
17400
17546
|
- ref(id): Get element by ref ID, chainable with all ElementHandle methods
|
|
17547
|
+
- ref(id).source(): Get React component source {filePath, lineNumber, componentName}
|
|
17548
|
+
- ref(id).props(): Get React component props (serialized)
|
|
17549
|
+
- ref(id).state(): Get React component state/hooks (serialized)
|
|
17550
|
+
- component(name, opts?): Find elements by React component name. opts: {nth: number}
|
|
17401
17551
|
- fill(id, text): Clear and fill input (works with rich text editors)
|
|
17402
17552
|
- drag({from, to, dataTransfer?}): Drag with custom MIME types
|
|
17403
17553
|
- dispatch({target, event, dataTransfer?, detail?}): Dispatch custom events
|
|
17404
17554
|
- waitFor(target): Wait for selector/ref/state. e.g. waitFor('e1'), waitFor('networkidle')
|
|
17405
17555
|
- grab: React Grab client API (activate, deactivate, toggle, isActive, copyElement, getState)
|
|
17406
17556
|
|
|
17557
|
+
REACT-SPECIFIC PATTERNS:
|
|
17558
|
+
- Get React source: return await ref('e1').source()
|
|
17559
|
+
- Get component props: return await ref('e1').props()
|
|
17560
|
+
- Get component state: return await ref('e1').state()
|
|
17561
|
+
- Find by component: const btn = await component('Button', {nth: 0})
|
|
17562
|
+
|
|
17407
17563
|
ELEMENT SCREENSHOTS (PREFERRED for visual issues):
|
|
17408
17564
|
- return await ref('e1').screenshot()
|
|
17409
|
-
- return await ref('e2').screenshot()
|
|
17410
17565
|
Use for: wrong color, broken styling, visual bugs, "how does X look", UI verification
|
|
17411
|
-
Returns image directly - no file path needed.
|
|
17412
17566
|
|
|
17413
17567
|
COMMON PATTERNS:
|
|
17414
17568
|
- Click: await ref('e1').click()
|
|
17415
17569
|
- Fill input: await fill('e1', 'hello')
|
|
17416
17570
|
- Get attribute: return await ref('e1').getAttribute('href')
|
|
17417
|
-
- Get React source: return await ref('e1').source()
|
|
17418
17571
|
- Navigate: await page.goto('https://example.com')
|
|
17419
17572
|
- Full page screenshot (rare): return await page.screenshot()
|
|
17420
17573
|
|
|
@@ -17434,13 +17587,9 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17434
17587
|
let pageOpenHandler = null;
|
|
17435
17588
|
const outputJson = createOutputJson(() => activePage, pageName);
|
|
17436
17589
|
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
|
-
}
|
|
17590
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17591
|
+
browser2 = connection.browser;
|
|
17592
|
+
activePage = connection.page;
|
|
17444
17593
|
if (url2) {
|
|
17445
17594
|
await activePage.goto(url2, {
|
|
17446
17595
|
waitUntil: "domcontentloaded",
|
|
@@ -17460,6 +17609,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17460
17609
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17461
17610
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17462
17611
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
17612
|
+
const component = createComponentHelper(getActivePage2);
|
|
17463
17613
|
const executeFunction = new Function(
|
|
17464
17614
|
"page",
|
|
17465
17615
|
"getActivePage",
|
|
@@ -17470,6 +17620,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17470
17620
|
"dispatch",
|
|
17471
17621
|
"grab",
|
|
17472
17622
|
"waitFor",
|
|
17623
|
+
"component",
|
|
17473
17624
|
`return (async () => { ${code} })();`
|
|
17474
17625
|
);
|
|
17475
17626
|
const result = await executeFunction(
|
|
@@ -17481,7 +17632,8 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17481
17632
|
drag,
|
|
17482
17633
|
dispatch,
|
|
17483
17634
|
grab,
|
|
17484
|
-
waitFor
|
|
17635
|
+
waitFor,
|
|
17636
|
+
component
|
|
17485
17637
|
);
|
|
17486
17638
|
if (Buffer.isBuffer(result)) {
|
|
17487
17639
|
const output2 = await outputJson(true, void 0);
|
|
@@ -17518,12 +17670,82 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17518
17670
|
}
|
|
17519
17671
|
}
|
|
17520
17672
|
);
|
|
17673
|
+
server.registerTool(
|
|
17674
|
+
"browser_react_tree",
|
|
17675
|
+
{
|
|
17676
|
+
description: `Get React component tree hierarchy (separate from ARIA tree).
|
|
17677
|
+
|
|
17678
|
+
Shows the React component structure with:
|
|
17679
|
+
- Component names and nesting
|
|
17680
|
+
- Source file locations
|
|
17681
|
+
- Element refs where available
|
|
17682
|
+
- Optional props (serialized)
|
|
17683
|
+
|
|
17684
|
+
Use this when you need to understand React component architecture rather than accessibility tree.
|
|
17685
|
+
For interacting with elements, use browser_snapshot to get refs first.`,
|
|
17686
|
+
inputSchema: {
|
|
17687
|
+
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17688
|
+
maxDepth: external_exports.number().optional().default(50).describe("Maximum tree depth"),
|
|
17689
|
+
includeProps: external_exports.boolean().optional().default(false).describe("Include component props (increases output size)")
|
|
17690
|
+
}
|
|
17691
|
+
},
|
|
17692
|
+
async ({ page: pageName, maxDepth, includeProps }) => {
|
|
17693
|
+
let browser2 = null;
|
|
17694
|
+
try {
|
|
17695
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17696
|
+
browser2 = connection.browser;
|
|
17697
|
+
const activePage = connection.page;
|
|
17698
|
+
const componentTree = await activePage.evaluate(
|
|
17699
|
+
async (opts2) => {
|
|
17700
|
+
const g2 = globalThis;
|
|
17701
|
+
if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
|
|
17702
|
+
return [];
|
|
17703
|
+
}
|
|
17704
|
+
return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
|
|
17705
|
+
},
|
|
17706
|
+
{ maxDepth: maxDepth ?? 50, includeProps: includeProps ?? false }
|
|
17707
|
+
);
|
|
17708
|
+
const renderTree = (nodes) => {
|
|
17709
|
+
const lines = [];
|
|
17710
|
+
for (const node of nodes) {
|
|
17711
|
+
const indent = " ".repeat(node.depth);
|
|
17712
|
+
let line = `${indent}- ${node.name}`;
|
|
17713
|
+
if (node.ref) line += ` [ref=${node.ref}]`;
|
|
17714
|
+
if (node.source) line += ` [source=${node.source}]`;
|
|
17715
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
17716
|
+
const propsStr = JSON.stringify(node.props);
|
|
17717
|
+
if (propsStr.length < 100) {
|
|
17718
|
+
line += ` [props=${propsStr}]`;
|
|
17719
|
+
} else {
|
|
17720
|
+
line += ` [props=...]`;
|
|
17721
|
+
}
|
|
17722
|
+
}
|
|
17723
|
+
lines.push(line);
|
|
17724
|
+
}
|
|
17725
|
+
return lines.join("\n");
|
|
17726
|
+
};
|
|
17727
|
+
const treeOutput = renderTree(componentTree);
|
|
17728
|
+
return {
|
|
17729
|
+
content: [
|
|
17730
|
+
{
|
|
17731
|
+
type: "text",
|
|
17732
|
+
text: treeOutput || "No React components found. Make sure react-grab is installed and the page uses React."
|
|
17733
|
+
}
|
|
17734
|
+
]
|
|
17735
|
+
};
|
|
17736
|
+
} catch (error48) {
|
|
17737
|
+
return createMcpErrorResponse(error48);
|
|
17738
|
+
} finally {
|
|
17739
|
+
await browser2?.close();
|
|
17740
|
+
}
|
|
17741
|
+
}
|
|
17742
|
+
);
|
|
17521
17743
|
const transport = new stdio_js.StdioServerTransport();
|
|
17522
17744
|
await server.connect(transport);
|
|
17523
17745
|
};
|
|
17524
17746
|
|
|
17525
17747
|
// src/commands/browser.ts
|
|
17526
|
-
var VERSION2 = "0.1.0";
|
|
17748
|
+
var VERSION2 = "0.1.0-beta.4";
|
|
17527
17749
|
var printHeader = () => {
|
|
17528
17750
|
console.log(
|
|
17529
17751
|
`${pc__default.default.magenta("\u273F")} ${pc__default.default.bold("React Grab")} ${pc__default.default.gray(VERSION2)}`
|
|
@@ -17546,6 +17768,10 @@ var rebuildNativeModuleAndRestart = async (browserPkgDir) => {
|
|
|
17546
17768
|
stdio: "inherit",
|
|
17547
17769
|
detached: false
|
|
17548
17770
|
});
|
|
17771
|
+
child.on("error", (error48) => {
|
|
17772
|
+
console.error(`Failed to restart: ${error48.message}`);
|
|
17773
|
+
process.exit(1);
|
|
17774
|
+
});
|
|
17549
17775
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
17550
17776
|
};
|
|
17551
17777
|
var isSupportedBrowser = (value) => {
|
|
@@ -17644,8 +17870,10 @@ var start = new commander.Command().name("start").description("start browser ser
|
|
|
17644
17870
|
const playwrightCookies = browser$1.toPlaywrightCookies(cookies);
|
|
17645
17871
|
const browser2 = await playwrightCore.chromium.connectOverCDP(browserServer.wsEndpoint);
|
|
17646
17872
|
const contexts = browser2.contexts();
|
|
17647
|
-
if (contexts.length > 0
|
|
17648
|
-
|
|
17873
|
+
if (contexts.length > 0) {
|
|
17874
|
+
if (playwrightCookies.length > 0) {
|
|
17875
|
+
await contexts[0].addCookies(playwrightCookies);
|
|
17876
|
+
}
|
|
17649
17877
|
await browser$1.applyStealthScripts(contexts[0]);
|
|
17650
17878
|
}
|
|
17651
17879
|
await browser2.close();
|
|
@@ -17777,6 +18005,7 @@ var execute = new commander.Command().name("execute").description("run Playwrigh
|
|
|
17777
18005
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17778
18006
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17779
18007
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
18008
|
+
const component = createComponentHelper(getActivePage2);
|
|
17780
18009
|
const executeFunction = new Function(
|
|
17781
18010
|
"page",
|
|
17782
18011
|
"getActivePage",
|
|
@@ -17787,9 +18016,10 @@ var execute = new commander.Command().name("execute").description("run Playwrigh
|
|
|
17787
18016
|
"dispatch",
|
|
17788
18017
|
"grab",
|
|
17789
18018
|
"waitFor",
|
|
18019
|
+
"component",
|
|
17790
18020
|
`return (async () => { ${code} })();`
|
|
17791
18021
|
);
|
|
17792
|
-
const result = await executeFunction(getActivePage2(), getActivePage2, snapshot, ref, fill, drag, dispatch, grab, waitFor);
|
|
18022
|
+
const result = await executeFunction(getActivePage2(), getActivePage2, snapshot, ref, fill, drag, dispatch, grab, waitFor, component);
|
|
17793
18023
|
console.log(JSON.stringify(await buildOutput(true, result)));
|
|
17794
18024
|
} catch (error48) {
|
|
17795
18025
|
console.log(JSON.stringify(await buildOutput(false, void 0, error48 instanceof Error ? error48.message : "Failed")));
|
|
@@ -17847,8 +18077,7 @@ PERFORMANCE TIPS
|
|
|
17847
18077
|
1. Batch multiple actions in a single execute call to minimize round-trips.
|
|
17848
18078
|
Each execute spawns a new connection, so combining actions is 3-5x faster.
|
|
17849
18079
|
|
|
17850
|
-
2. Use
|
|
17851
|
-
- snapshot({format: 'compact'}) -> minimal ref:role:name output
|
|
18080
|
+
2. Use interactableOnly or limit depth for smaller snapshots (faster, fewer tokens).
|
|
17852
18081
|
- snapshot({interactableOnly: true}) -> only clickable/input elements
|
|
17853
18082
|
- snapshot({maxDepth: 5}) -> limit tree depth
|
|
17854
18083
|
|
|
@@ -17857,22 +18086,28 @@ PERFORMANCE TIPS
|
|
|
17857
18086
|
execute "await ref('e1').click()"
|
|
17858
18087
|
execute "return await snapshot()"
|
|
17859
18088
|
|
|
17860
|
-
# FAST: 1 round-trip,
|
|
18089
|
+
# FAST: 1 round-trip, interactable only
|
|
17861
18090
|
execute "
|
|
17862
18091
|
await page.goto('https://example.com');
|
|
17863
18092
|
await ref('e1').click();
|
|
17864
|
-
return await snapshot({
|
|
18093
|
+
return await snapshot({interactableOnly: true});
|
|
17865
18094
|
"
|
|
17866
18095
|
|
|
17867
18096
|
HELPERS
|
|
17868
18097
|
page - Playwright Page object
|
|
17869
18098
|
snapshot(opts?) - Get ARIA accessibility tree with refs
|
|
17870
18099
|
opts.maxDepth: limit tree depth (e.g., 5)
|
|
17871
|
-
opts.interactableOnly: only
|
|
17872
|
-
opts.format: "yaml" (default) or "compact"
|
|
18100
|
+
opts.interactableOnly: only clickable/input elements
|
|
17873
18101
|
ref(id) - Get element by ref ID (chainable - supports all ElementHandle methods)
|
|
17874
18102
|
Example: await ref('e1').click()
|
|
17875
18103
|
Example: await ref('e1').getAttribute('data-foo')
|
|
18104
|
+
ref(id).source() - Get React component source file info for element
|
|
18105
|
+
Returns { filePath, lineNumber, componentName } or null
|
|
18106
|
+
ref(id).props() - Get React component props (serialized)
|
|
18107
|
+
ref(id).state() - Get React component state/hooks (serialized)
|
|
18108
|
+
component(name, opts?) - Find elements by React component name
|
|
18109
|
+
opts.nth: get the nth matching element (0-indexed)
|
|
18110
|
+
Example: await component('Button', {nth: 0})
|
|
17876
18111
|
fill(id, text) - Clear and fill input (works with rich text editors)
|
|
17877
18112
|
drag(opts) - Drag with custom MIME types
|
|
17878
18113
|
opts.from: source selector or ref ID (e.g., "e1" or "text=src")
|
|
@@ -17888,21 +18123,16 @@ HELPERS
|
|
|
17888
18123
|
await waitFor('.btn') - wait for selector
|
|
17889
18124
|
await waitFor('networkidle') - wait for network idle
|
|
17890
18125
|
await waitFor('load') - wait for page load
|
|
17891
|
-
ref(id).source() - Get React component source file info for element
|
|
17892
|
-
Returns { filePath, lineNumber, componentName } or null
|
|
17893
18126
|
grab - React Grab client API (activate, copyElement, etc)
|
|
17894
18127
|
|
|
17895
|
-
SNAPSHOT
|
|
18128
|
+
SNAPSHOT OPTIONS
|
|
17896
18129
|
# Full YAML tree (default, can be large)
|
|
17897
18130
|
execute "return await snapshot()"
|
|
17898
18131
|
|
|
17899
18132
|
# Interactable only (recommended - much smaller!)
|
|
17900
18133
|
execute "return await snapshot({interactableOnly: true})"
|
|
17901
18134
|
|
|
17902
|
-
#
|
|
17903
|
-
execute "return await snapshot({format: 'compact'})"
|
|
17904
|
-
|
|
17905
|
-
# Combined options
|
|
18135
|
+
# With depth limit
|
|
17906
18136
|
execute "return await snapshot({interactableOnly: true, maxDepth: 6})"
|
|
17907
18137
|
|
|
17908
18138
|
SCREENSHOTS - PREFER ELEMENT OVER FULL PAGE
|
|
@@ -17946,15 +18176,28 @@ COMMON PATTERNS
|
|
|
17946
18176
|
dataTransfer: { 'application/x-custom': 'data' }
|
|
17947
18177
|
})"
|
|
17948
18178
|
|
|
17949
|
-
# Get React component source file
|
|
17950
|
-
execute "return await ref('e1').source()"
|
|
17951
|
-
|
|
17952
18179
|
# Get page info
|
|
17953
18180
|
execute "return {url: page.url(), title: await page.title()}"
|
|
17954
18181
|
|
|
17955
18182
|
# CSS selector fallback (refs are now in DOM as aria-ref)
|
|
17956
18183
|
execute "await page.click('[aria-ref="e1"]')"
|
|
17957
18184
|
|
|
18185
|
+
REACT-SPECIFIC PATTERNS
|
|
18186
|
+
# Get React component source file
|
|
18187
|
+
execute "return await ref('e1').source()"
|
|
18188
|
+
|
|
18189
|
+
# Get component props
|
|
18190
|
+
execute "return await ref('e1').props()"
|
|
18191
|
+
|
|
18192
|
+
# Get component state
|
|
18193
|
+
execute "return await ref('e1').state()"
|
|
18194
|
+
|
|
18195
|
+
# Find elements by React component name
|
|
18196
|
+
execute "const buttons = await component('Button'); return buttons.length"
|
|
18197
|
+
|
|
18198
|
+
# Get the first Button component and click it
|
|
18199
|
+
execute "const btn = await component('Button', {nth: 0}); await btn.click()"
|
|
18200
|
+
|
|
17958
18201
|
MULTI-PAGE SESSIONS
|
|
17959
18202
|
execute "await page.goto('https://github.com')" --page github
|
|
17960
18203
|
execute "return await snapshot({interactableOnly: true})" --page github
|
|
@@ -18011,14 +18254,7 @@ browser.addCommand(status);
|
|
|
18011
18254
|
browser.addCommand(execute);
|
|
18012
18255
|
browser.addCommand(pages);
|
|
18013
18256
|
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";
|
|
18257
|
+
var VERSION3 = "0.1.0-beta.4";
|
|
18022
18258
|
var isMac = process.platform === "darwin";
|
|
18023
18259
|
var META_LABEL = isMac ? "Cmd" : "Win";
|
|
18024
18260
|
var ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -18505,7 +18741,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
|
|
|
18505
18741
|
handleError(error48);
|
|
18506
18742
|
}
|
|
18507
18743
|
};
|
|
18508
|
-
var VERSION4 = "0.1.0";
|
|
18744
|
+
var VERSION4 = "0.1.0-beta.4";
|
|
18509
18745
|
var REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
18510
18746
|
var DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
18511
18747
|
var promptAgentIntegration = async (cwd, customPkg) => {
|
|
@@ -18549,19 +18785,29 @@ var promptAgentIntegration = async (cwd, customPkg) => {
|
|
|
18549
18785
|
}
|
|
18550
18786
|
}
|
|
18551
18787
|
if (integrationType === "skill" || integrationType === "both") {
|
|
18552
|
-
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
|
|
18561
|
-
|
|
18788
|
+
const { skillTarget } = await prompts3__default.default({
|
|
18789
|
+
type: "select",
|
|
18790
|
+
name: "skillTarget",
|
|
18791
|
+
message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
|
|
18792
|
+
choices: SUPPORTED_TARGETS.map((target) => ({
|
|
18793
|
+
title: target,
|
|
18794
|
+
value: target
|
|
18795
|
+
}))
|
|
18796
|
+
});
|
|
18797
|
+
if (skillTarget) {
|
|
18562
18798
|
logger.break();
|
|
18563
|
-
skillSpinner
|
|
18564
|
-
|
|
18799
|
+
const skillSpinner = spinner("Installing browser automation skill").start();
|
|
18800
|
+
try {
|
|
18801
|
+
const skill = await fetchSkillFile();
|
|
18802
|
+
const skillDir = path.join(cwd, AGENT_TARGETS[skillTarget]);
|
|
18803
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
18804
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
18805
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), skill);
|
|
18806
|
+
skillSpinner.succeed(`Skill installed to ${AGENT_TARGETS[skillTarget]}/`);
|
|
18807
|
+
} catch {
|
|
18808
|
+
skillSpinner.fail("Failed to install skill");
|
|
18809
|
+
logger.dim("Try manually: npx -y openskills install aidenybai/react-grab");
|
|
18810
|
+
}
|
|
18565
18811
|
}
|
|
18566
18812
|
}
|
|
18567
18813
|
logger.break();
|
|
@@ -19197,7 +19443,7 @@ var init = new commander.Command().name("init").description("initialize React Gr
|
|
|
19197
19443
|
await reportToCli("error", void 0, error48);
|
|
19198
19444
|
}
|
|
19199
19445
|
});
|
|
19200
|
-
var VERSION5 = "0.1.0";
|
|
19446
|
+
var VERSION5 = "0.1.0-beta.4";
|
|
19201
19447
|
var remove = new commander.Command().name("remove").description("remove an agent integration").argument(
|
|
19202
19448
|
"[agent]",
|
|
19203
19449
|
"agent to remove (claude-code, cursor, opencode, codex, gemini, amp, ami, visual-edit)"
|
|
@@ -19376,7 +19622,7 @@ var remove = new commander.Command().name("remove").description("remove an agent
|
|
|
19376
19622
|
});
|
|
19377
19623
|
|
|
19378
19624
|
// src/cli.ts
|
|
19379
|
-
var VERSION6 = "0.1.0";
|
|
19625
|
+
var VERSION6 = "0.1.0-beta.4";
|
|
19380
19626
|
var VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
19381
19627
|
process.on("SIGINT", () => process.exit(0));
|
|
19382
19628
|
process.on("SIGTERM", () => process.exit(0));
|