@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.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.4";
|
|
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
|
};
|
|
@@ -3317,12 +3375,42 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3317
3375
|
};
|
|
3318
3376
|
const getSource = async (refId) => {
|
|
3319
3377
|
const element = await getElement(refId);
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3378
|
+
try {
|
|
3379
|
+
const currentPage2 = getActivePage2();
|
|
3380
|
+
return await currentPage2.evaluate((el) => {
|
|
3381
|
+
const g2 = globalThis;
|
|
3382
|
+
if (!g2.__REACT_GRAB__) return null;
|
|
3383
|
+
return g2.__REACT_GRAB__.getSource(el);
|
|
3384
|
+
}, element);
|
|
3385
|
+
} finally {
|
|
3386
|
+
await element.dispose();
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3389
|
+
const getProps = async (refId) => {
|
|
3390
|
+
const element = await getElement(refId);
|
|
3391
|
+
try {
|
|
3392
|
+
const currentPage2 = getActivePage2();
|
|
3393
|
+
return await currentPage2.evaluate((el) => {
|
|
3394
|
+
const g2 = globalThis;
|
|
3395
|
+
if (!g2.__REACT_GRAB_GET_PROPS__) return null;
|
|
3396
|
+
return g2.__REACT_GRAB_GET_PROPS__(el);
|
|
3397
|
+
}, element);
|
|
3398
|
+
} finally {
|
|
3399
|
+
await element.dispose();
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
const getState = async (refId) => {
|
|
3403
|
+
const element = await getElement(refId);
|
|
3404
|
+
try {
|
|
3405
|
+
const currentPage2 = getActivePage2();
|
|
3406
|
+
return await currentPage2.evaluate((el) => {
|
|
3407
|
+
const g2 = globalThis;
|
|
3408
|
+
if (!g2.__REACT_GRAB_GET_STATE__) return null;
|
|
3409
|
+
return g2.__REACT_GRAB_GET_STATE__(el);
|
|
3410
|
+
}, element);
|
|
3411
|
+
} finally {
|
|
3412
|
+
await element.dispose();
|
|
3413
|
+
}
|
|
3326
3414
|
};
|
|
3327
3415
|
return (refId) => {
|
|
3328
3416
|
return new Proxy(
|
|
@@ -3335,19 +3423,79 @@ var createRefHelper = (getActivePage2) => {
|
|
|
3335
3423
|
if (prop === "source") {
|
|
3336
3424
|
return () => getSource(refId);
|
|
3337
3425
|
}
|
|
3426
|
+
if (prop === "props") {
|
|
3427
|
+
return () => getProps(refId);
|
|
3428
|
+
}
|
|
3429
|
+
if (prop === "state") {
|
|
3430
|
+
return () => getState(refId);
|
|
3431
|
+
}
|
|
3338
3432
|
if (prop === "screenshot") {
|
|
3339
|
-
return (options2) =>
|
|
3340
|
-
|
|
3341
|
-
|
|
3433
|
+
return async (options2) => {
|
|
3434
|
+
const el = await getElement(refId);
|
|
3435
|
+
try {
|
|
3436
|
+
return await el.screenshot({ scale: "css", ...options2 });
|
|
3437
|
+
} finally {
|
|
3438
|
+
await el.dispose();
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3342
3441
|
}
|
|
3343
|
-
return (...args) =>
|
|
3344
|
-
|
|
3345
|
-
|
|
3442
|
+
return async (...args) => {
|
|
3443
|
+
const el = await getElement(refId);
|
|
3444
|
+
try {
|
|
3445
|
+
return await el[prop](...args);
|
|
3446
|
+
} finally {
|
|
3447
|
+
await el.dispose();
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3346
3450
|
}
|
|
3347
3451
|
}
|
|
3348
3452
|
);
|
|
3349
3453
|
};
|
|
3350
3454
|
};
|
|
3455
|
+
var createComponentHelper = (getActivePage2) => {
|
|
3456
|
+
return async (componentName, options2) => {
|
|
3457
|
+
const currentPage2 = getActivePage2();
|
|
3458
|
+
const nth = options2?.nth;
|
|
3459
|
+
const elementHandles = await currentPage2.evaluateHandle(
|
|
3460
|
+
async (args) => {
|
|
3461
|
+
const g2 = globalThis;
|
|
3462
|
+
if (!g2.__REACT_GRAB_FIND_BY_COMPONENT__) {
|
|
3463
|
+
throw new Error("React introspection not available. Make sure react-grab is installed.");
|
|
3464
|
+
}
|
|
3465
|
+
const result = await g2.__REACT_GRAB_FIND_BY_COMPONENT__(args.name, args.nth !== void 0 ? { nth: args.nth } : void 0);
|
|
3466
|
+
if (!result) return null;
|
|
3467
|
+
if (args.nth !== void 0) {
|
|
3468
|
+
const single = result;
|
|
3469
|
+
return single?.element || null;
|
|
3470
|
+
}
|
|
3471
|
+
const arr = result;
|
|
3472
|
+
return arr.map((m2) => m2.element);
|
|
3473
|
+
},
|
|
3474
|
+
{ name: componentName, nth }
|
|
3475
|
+
);
|
|
3476
|
+
const isNull = await currentPage2.evaluate((value) => value === null, elementHandles);
|
|
3477
|
+
if (isNull) {
|
|
3478
|
+
await elementHandles.dispose();
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
if (nth !== void 0) {
|
|
3482
|
+
const element = elementHandles.asElement();
|
|
3483
|
+
if (!element) {
|
|
3484
|
+
await elementHandles.dispose();
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
return element;
|
|
3488
|
+
}
|
|
3489
|
+
const jsHandles = await elementHandles.getProperties();
|
|
3490
|
+
const handles = [];
|
|
3491
|
+
for (const [, handle] of jsHandles) {
|
|
3492
|
+
const element = handle.asElement();
|
|
3493
|
+
if (element) handles.push(element);
|
|
3494
|
+
}
|
|
3495
|
+
await elementHandles.dispose();
|
|
3496
|
+
return handles;
|
|
3497
|
+
};
|
|
3498
|
+
};
|
|
3351
3499
|
var createFillHelper = (ref, getActivePage2) => {
|
|
3352
3500
|
return async (refId, text) => {
|
|
3353
3501
|
const element = await ref(refId);
|
|
@@ -3449,50 +3597,34 @@ var createDispatchHelper = (getActivePage2) => {
|
|
|
3449
3597
|
};
|
|
3450
3598
|
};
|
|
3451
3599
|
var createGrabHelper = (ref, getActivePage2) => {
|
|
3600
|
+
const evaluateGrabMethod = async (methodName, defaultValue) => {
|
|
3601
|
+
const currentPage2 = getActivePage2();
|
|
3602
|
+
return currentPage2.evaluate(
|
|
3603
|
+
({ method, fallback }) => {
|
|
3604
|
+
const grab = globalThis.__REACT_GRAB__;
|
|
3605
|
+
return grab?.[method]?.() ?? fallback;
|
|
3606
|
+
},
|
|
3607
|
+
{ method: methodName, fallback: defaultValue }
|
|
3608
|
+
);
|
|
3609
|
+
};
|
|
3452
3610
|
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
|
-
},
|
|
3611
|
+
activate: () => evaluateGrabMethod("activate", void 0),
|
|
3612
|
+
deactivate: () => evaluateGrabMethod("deactivate", void 0),
|
|
3613
|
+
toggle: () => evaluateGrabMethod("toggle", void 0),
|
|
3614
|
+
isActive: () => evaluateGrabMethod("isActive", false),
|
|
3615
|
+
getState: () => evaluateGrabMethod("getState", null),
|
|
3481
3616
|
copyElement: async (refId) => {
|
|
3482
3617
|
const element = await ref(refId);
|
|
3483
3618
|
if (!element) return false;
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
const g2 = globalThis;
|
|
3494
|
-
return g2.__REACT_GRAB__?.getState() ?? null;
|
|
3495
|
-
});
|
|
3619
|
+
try {
|
|
3620
|
+
const currentPage2 = getActivePage2();
|
|
3621
|
+
return await currentPage2.evaluate((el) => {
|
|
3622
|
+
const g2 = globalThis;
|
|
3623
|
+
return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
|
|
3624
|
+
}, element);
|
|
3625
|
+
} finally {
|
|
3626
|
+
await element.dispose();
|
|
3627
|
+
}
|
|
3496
3628
|
}
|
|
3497
3629
|
};
|
|
3498
3630
|
};
|
|
@@ -3508,7 +3640,7 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3508
3640
|
return async (selectorOrState, options2) => {
|
|
3509
3641
|
const currentPage2 = getActivePage2();
|
|
3510
3642
|
const timeout = options2?.timeout;
|
|
3511
|
-
if (selectorOrState
|
|
3643
|
+
if (LOAD_STATES.has(selectorOrState)) {
|
|
3512
3644
|
await currentPage2.waitForLoadState(selectorOrState, { timeout });
|
|
3513
3645
|
return;
|
|
3514
3646
|
}
|
|
@@ -3519,6 +3651,33 @@ var createWaitForHelper = (getActivePage2) => {
|
|
|
3519
3651
|
await currentPage2.waitForSelector(selectorOrState, { timeout });
|
|
3520
3652
|
};
|
|
3521
3653
|
};
|
|
3654
|
+
var connectToBrowserPage = async (pageName) => {
|
|
3655
|
+
const { chromium: chromium2 } = await import('playwright-core');
|
|
3656
|
+
const { findPageByTargetId: findPageByTargetId2 } = await import('@react-grab/browser');
|
|
3657
|
+
const { serverUrl } = await ensureHealthyServer();
|
|
3658
|
+
const pageInfo = await getOrCreatePage(serverUrl, pageName);
|
|
3659
|
+
const browser2 = await chromium2.connectOverCDP(pageInfo.wsEndpoint);
|
|
3660
|
+
const page = await findPageByTargetId2(browser2, pageInfo.targetId);
|
|
3661
|
+
if (!page) {
|
|
3662
|
+
await browser2.close();
|
|
3663
|
+
throw new Error(`Page "${pageName}" not found`);
|
|
3664
|
+
}
|
|
3665
|
+
return { browser: browser2, page, serverUrl };
|
|
3666
|
+
};
|
|
3667
|
+
var createMcpErrorResponse = (error48) => {
|
|
3668
|
+
return {
|
|
3669
|
+
content: [
|
|
3670
|
+
{
|
|
3671
|
+
type: "text",
|
|
3672
|
+
text: JSON.stringify({
|
|
3673
|
+
ok: false,
|
|
3674
|
+
error: error48 instanceof Error ? error48.message : "Failed"
|
|
3675
|
+
})
|
|
3676
|
+
}
|
|
3677
|
+
],
|
|
3678
|
+
isError: true
|
|
3679
|
+
};
|
|
3680
|
+
};
|
|
3522
3681
|
|
|
3523
3682
|
// ../../node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/external.js
|
|
3524
3683
|
var external_exports = {};
|
|
@@ -17288,7 +17447,13 @@ var startMcpServer = async () => {
|
|
|
17288
17447
|
server.registerTool(
|
|
17289
17448
|
"browser_snapshot",
|
|
17290
17449
|
{
|
|
17291
|
-
description: `Get ARIA accessibility tree with element refs (e1, e2...).
|
|
17450
|
+
description: `Get ARIA accessibility tree with element refs (e1, e2...) and React component info.
|
|
17451
|
+
|
|
17452
|
+
OUTPUT INCLUDES:
|
|
17453
|
+
- ARIA roles and accessible names
|
|
17454
|
+
- Element refs (e1, e2...) for interaction
|
|
17455
|
+
- [component=ComponentName] for React components
|
|
17456
|
+
- [source=file.tsx:line] for source location
|
|
17292
17457
|
|
|
17293
17458
|
SCREENSHOT STRATEGY - ALWAYS prefer element screenshots over full page:
|
|
17294
17459
|
1. First: Get refs with snapshot (this tool)
|
|
@@ -17306,7 +17471,6 @@ USE VIEWPORT screenshot=true ONLY FOR:
|
|
|
17306
17471
|
|
|
17307
17472
|
PERFORMANCE:
|
|
17308
17473
|
- interactableOnly:true = much smaller output (recommended)
|
|
17309
|
-
- format:'compact' = minimal ref:role:name output
|
|
17310
17474
|
- maxDepth = limit tree depth
|
|
17311
17475
|
|
|
17312
17476
|
After getting refs, use browser_execute with: ref('e1').click()`,
|
|
@@ -17314,7 +17478,6 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17314
17478
|
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17315
17479
|
maxDepth: external_exports.number().optional().describe("Limit tree depth"),
|
|
17316
17480
|
interactableOnly: external_exports.boolean().optional().describe("Only clickable/input elements (recommended)"),
|
|
17317
|
-
format: external_exports.enum(["yaml", "compact"]).optional().default("yaml").describe("'yaml' or 'compact'"),
|
|
17318
17481
|
screenshot: external_exports.boolean().optional().default(false).describe(
|
|
17319
17482
|
"Viewport screenshot. For element screenshots (PREFERRED), use browser_execute: ref('eX').screenshot()"
|
|
17320
17483
|
)
|
|
@@ -17324,22 +17487,16 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17324
17487
|
page: pageName,
|
|
17325
17488
|
maxDepth,
|
|
17326
17489
|
interactableOnly,
|
|
17327
|
-
format,
|
|
17328
17490
|
screenshot
|
|
17329
17491
|
}) => {
|
|
17330
|
-
let activePage = null;
|
|
17331
17492
|
let browser2 = null;
|
|
17332
17493
|
try {
|
|
17333
|
-
const
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
activePage = await findPageByTargetId(browser2, pageInfo.targetId);
|
|
17337
|
-
if (!activePage) {
|
|
17338
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17339
|
-
}
|
|
17494
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17495
|
+
browser2 = connection.browser;
|
|
17496
|
+
const activePage = connection.page;
|
|
17340
17497
|
const getActivePage2 = () => activePage;
|
|
17341
17498
|
const snapshot = createSnapshotHelper(getActivePage2);
|
|
17342
|
-
const snapshotResult = await snapshot({ maxDepth, interactableOnly
|
|
17499
|
+
const snapshotResult = await snapshot({ maxDepth, interactableOnly });
|
|
17343
17500
|
if (screenshot) {
|
|
17344
17501
|
const screenshotBuffer = await activePage.screenshot({
|
|
17345
17502
|
fullPage: false,
|
|
@@ -17360,18 +17517,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
|
|
|
17360
17517
|
content: [{ type: "text", text: snapshotResult }]
|
|
17361
17518
|
};
|
|
17362
17519
|
} 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
|
-
};
|
|
17520
|
+
return createMcpErrorResponse(error48);
|
|
17375
17521
|
} finally {
|
|
17376
17522
|
await browser2?.close();
|
|
17377
17523
|
}
|
|
@@ -17386,25 +17532,32 @@ IMPORTANT: Always call snapshot() first to get element refs from the a11y tree (
|
|
|
17386
17532
|
|
|
17387
17533
|
AVAILABLE HELPERS:
|
|
17388
17534
|
- page: Playwright Page object (https://playwright.dev/docs/api/class-page)
|
|
17389
|
-
- snapshot(opts?): Get ARIA tree. opts: {maxDepth, interactableOnly
|
|
17535
|
+
- snapshot(opts?): Get ARIA tree with React component info. opts: {maxDepth, interactableOnly}
|
|
17390
17536
|
- ref(id): Get element by ref ID, chainable with all ElementHandle methods
|
|
17537
|
+
- ref(id).source(): Get React component source {filePath, lineNumber, componentName}
|
|
17538
|
+
- ref(id).props(): Get React component props (serialized)
|
|
17539
|
+
- ref(id).state(): Get React component state/hooks (serialized)
|
|
17540
|
+
- component(name, opts?): Find elements by React component name. opts: {nth: number}
|
|
17391
17541
|
- fill(id, text): Clear and fill input (works with rich text editors)
|
|
17392
17542
|
- drag({from, to, dataTransfer?}): Drag with custom MIME types
|
|
17393
17543
|
- dispatch({target, event, dataTransfer?, detail?}): Dispatch custom events
|
|
17394
17544
|
- waitFor(target): Wait for selector/ref/state. e.g. waitFor('e1'), waitFor('networkidle')
|
|
17395
17545
|
- grab: React Grab client API (activate, deactivate, toggle, isActive, copyElement, getState)
|
|
17396
17546
|
|
|
17547
|
+
REACT-SPECIFIC PATTERNS:
|
|
17548
|
+
- Get React source: return await ref('e1').source()
|
|
17549
|
+
- Get component props: return await ref('e1').props()
|
|
17550
|
+
- Get component state: return await ref('e1').state()
|
|
17551
|
+
- Find by component: const btn = await component('Button', {nth: 0})
|
|
17552
|
+
|
|
17397
17553
|
ELEMENT SCREENSHOTS (PREFERRED for visual issues):
|
|
17398
17554
|
- return await ref('e1').screenshot()
|
|
17399
|
-
- return await ref('e2').screenshot()
|
|
17400
17555
|
Use for: wrong color, broken styling, visual bugs, "how does X look", UI verification
|
|
17401
|
-
Returns image directly - no file path needed.
|
|
17402
17556
|
|
|
17403
17557
|
COMMON PATTERNS:
|
|
17404
17558
|
- Click: await ref('e1').click()
|
|
17405
17559
|
- Fill input: await fill('e1', 'hello')
|
|
17406
17560
|
- Get attribute: return await ref('e1').getAttribute('href')
|
|
17407
|
-
- Get React source: return await ref('e1').source()
|
|
17408
17561
|
- Navigate: await page.goto('https://example.com')
|
|
17409
17562
|
- Full page screenshot (rare): return await page.screenshot()
|
|
17410
17563
|
|
|
@@ -17424,13 +17577,9 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17424
17577
|
let pageOpenHandler = null;
|
|
17425
17578
|
const outputJson = createOutputJson(() => activePage, pageName);
|
|
17426
17579
|
try {
|
|
17427
|
-
const
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
activePage = await findPageByTargetId(browser2, pageInfo.targetId);
|
|
17431
|
-
if (!activePage) {
|
|
17432
|
-
throw new Error(`Page "${pageName}" not found`);
|
|
17433
|
-
}
|
|
17580
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17581
|
+
browser2 = connection.browser;
|
|
17582
|
+
activePage = connection.page;
|
|
17434
17583
|
if (url2) {
|
|
17435
17584
|
await activePage.goto(url2, {
|
|
17436
17585
|
waitUntil: "domcontentloaded",
|
|
@@ -17450,6 +17599,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17450
17599
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17451
17600
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17452
17601
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
17602
|
+
const component = createComponentHelper(getActivePage2);
|
|
17453
17603
|
const executeFunction = new Function(
|
|
17454
17604
|
"page",
|
|
17455
17605
|
"getActivePage",
|
|
@@ -17460,6 +17610,7 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17460
17610
|
"dispatch",
|
|
17461
17611
|
"grab",
|
|
17462
17612
|
"waitFor",
|
|
17613
|
+
"component",
|
|
17463
17614
|
`return (async () => { ${code} })();`
|
|
17464
17615
|
);
|
|
17465
17616
|
const result = await executeFunction(
|
|
@@ -17471,7 +17622,8 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17471
17622
|
drag,
|
|
17472
17623
|
dispatch,
|
|
17473
17624
|
grab,
|
|
17474
|
-
waitFor
|
|
17625
|
+
waitFor,
|
|
17626
|
+
component
|
|
17475
17627
|
);
|
|
17476
17628
|
if (Buffer.isBuffer(result)) {
|
|
17477
17629
|
const output2 = await outputJson(true, void 0);
|
|
@@ -17508,12 +17660,82 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
|
|
|
17508
17660
|
}
|
|
17509
17661
|
}
|
|
17510
17662
|
);
|
|
17663
|
+
server.registerTool(
|
|
17664
|
+
"browser_react_tree",
|
|
17665
|
+
{
|
|
17666
|
+
description: `Get React component tree hierarchy (separate from ARIA tree).
|
|
17667
|
+
|
|
17668
|
+
Shows the React component structure with:
|
|
17669
|
+
- Component names and nesting
|
|
17670
|
+
- Source file locations
|
|
17671
|
+
- Element refs where available
|
|
17672
|
+
- Optional props (serialized)
|
|
17673
|
+
|
|
17674
|
+
Use this when you need to understand React component architecture rather than accessibility tree.
|
|
17675
|
+
For interacting with elements, use browser_snapshot to get refs first.`,
|
|
17676
|
+
inputSchema: {
|
|
17677
|
+
page: external_exports.string().optional().default("default").describe("Named page context"),
|
|
17678
|
+
maxDepth: external_exports.number().optional().default(50).describe("Maximum tree depth"),
|
|
17679
|
+
includeProps: external_exports.boolean().optional().default(false).describe("Include component props (increases output size)")
|
|
17680
|
+
}
|
|
17681
|
+
},
|
|
17682
|
+
async ({ page: pageName, maxDepth, includeProps }) => {
|
|
17683
|
+
let browser2 = null;
|
|
17684
|
+
try {
|
|
17685
|
+
const connection = await connectToBrowserPage(pageName);
|
|
17686
|
+
browser2 = connection.browser;
|
|
17687
|
+
const activePage = connection.page;
|
|
17688
|
+
const componentTree = await activePage.evaluate(
|
|
17689
|
+
async (opts2) => {
|
|
17690
|
+
const g2 = globalThis;
|
|
17691
|
+
if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
|
|
17692
|
+
return [];
|
|
17693
|
+
}
|
|
17694
|
+
return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
|
|
17695
|
+
},
|
|
17696
|
+
{ maxDepth: maxDepth ?? 50, includeProps: includeProps ?? false }
|
|
17697
|
+
);
|
|
17698
|
+
const renderTree = (nodes) => {
|
|
17699
|
+
const lines = [];
|
|
17700
|
+
for (const node of nodes) {
|
|
17701
|
+
const indent = " ".repeat(node.depth);
|
|
17702
|
+
let line = `${indent}- ${node.name}`;
|
|
17703
|
+
if (node.ref) line += ` [ref=${node.ref}]`;
|
|
17704
|
+
if (node.source) line += ` [source=${node.source}]`;
|
|
17705
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
17706
|
+
const propsStr = JSON.stringify(node.props);
|
|
17707
|
+
if (propsStr.length < 100) {
|
|
17708
|
+
line += ` [props=${propsStr}]`;
|
|
17709
|
+
} else {
|
|
17710
|
+
line += ` [props=...]`;
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
17713
|
+
lines.push(line);
|
|
17714
|
+
}
|
|
17715
|
+
return lines.join("\n");
|
|
17716
|
+
};
|
|
17717
|
+
const treeOutput = renderTree(componentTree);
|
|
17718
|
+
return {
|
|
17719
|
+
content: [
|
|
17720
|
+
{
|
|
17721
|
+
type: "text",
|
|
17722
|
+
text: treeOutput || "No React components found. Make sure react-grab is installed and the page uses React."
|
|
17723
|
+
}
|
|
17724
|
+
]
|
|
17725
|
+
};
|
|
17726
|
+
} catch (error48) {
|
|
17727
|
+
return createMcpErrorResponse(error48);
|
|
17728
|
+
} finally {
|
|
17729
|
+
await browser2?.close();
|
|
17730
|
+
}
|
|
17731
|
+
}
|
|
17732
|
+
);
|
|
17511
17733
|
const transport = new StdioServerTransport();
|
|
17512
17734
|
await server.connect(transport);
|
|
17513
17735
|
};
|
|
17514
17736
|
|
|
17515
17737
|
// src/commands/browser.ts
|
|
17516
|
-
var VERSION2 = "0.1.0";
|
|
17738
|
+
var VERSION2 = "0.1.0-beta.4";
|
|
17517
17739
|
var printHeader = () => {
|
|
17518
17740
|
console.log(
|
|
17519
17741
|
`${pc.magenta("\u273F")} ${pc.bold("React Grab")} ${pc.gray(VERSION2)}`
|
|
@@ -17536,6 +17758,10 @@ var rebuildNativeModuleAndRestart = async (browserPkgDir) => {
|
|
|
17536
17758
|
stdio: "inherit",
|
|
17537
17759
|
detached: false
|
|
17538
17760
|
});
|
|
17761
|
+
child.on("error", (error48) => {
|
|
17762
|
+
console.error(`Failed to restart: ${error48.message}`);
|
|
17763
|
+
process.exit(1);
|
|
17764
|
+
});
|
|
17539
17765
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
17540
17766
|
};
|
|
17541
17767
|
var isSupportedBrowser = (value) => {
|
|
@@ -17634,8 +17860,10 @@ var start = new Command().name("start").description("start browser server manual
|
|
|
17634
17860
|
const playwrightCookies = toPlaywrightCookies(cookies);
|
|
17635
17861
|
const browser2 = await chromium.connectOverCDP(browserServer.wsEndpoint);
|
|
17636
17862
|
const contexts = browser2.contexts();
|
|
17637
|
-
if (contexts.length > 0
|
|
17638
|
-
|
|
17863
|
+
if (contexts.length > 0) {
|
|
17864
|
+
if (playwrightCookies.length > 0) {
|
|
17865
|
+
await contexts[0].addCookies(playwrightCookies);
|
|
17866
|
+
}
|
|
17639
17867
|
await applyStealthScripts(contexts[0]);
|
|
17640
17868
|
}
|
|
17641
17869
|
await browser2.close();
|
|
@@ -17767,6 +17995,7 @@ var execute = new Command().name("execute").description("run Playwright code wit
|
|
|
17767
17995
|
const dispatch = createDispatchHelper(getActivePage2);
|
|
17768
17996
|
const grab = createGrabHelper(ref, getActivePage2);
|
|
17769
17997
|
const waitFor = createWaitForHelper(getActivePage2);
|
|
17998
|
+
const component = createComponentHelper(getActivePage2);
|
|
17770
17999
|
const executeFunction = new Function(
|
|
17771
18000
|
"page",
|
|
17772
18001
|
"getActivePage",
|
|
@@ -17777,9 +18006,10 @@ var execute = new Command().name("execute").description("run Playwright code wit
|
|
|
17777
18006
|
"dispatch",
|
|
17778
18007
|
"grab",
|
|
17779
18008
|
"waitFor",
|
|
18009
|
+
"component",
|
|
17780
18010
|
`return (async () => { ${code} })();`
|
|
17781
18011
|
);
|
|
17782
|
-
const result = await executeFunction(getActivePage2(), getActivePage2, snapshot, ref, fill, drag, dispatch, grab, waitFor);
|
|
18012
|
+
const result = await executeFunction(getActivePage2(), getActivePage2, snapshot, ref, fill, drag, dispatch, grab, waitFor, component);
|
|
17783
18013
|
console.log(JSON.stringify(await buildOutput(true, result)));
|
|
17784
18014
|
} catch (error48) {
|
|
17785
18015
|
console.log(JSON.stringify(await buildOutput(false, void 0, error48 instanceof Error ? error48.message : "Failed")));
|
|
@@ -17837,8 +18067,7 @@ PERFORMANCE TIPS
|
|
|
17837
18067
|
1. Batch multiple actions in a single execute call to minimize round-trips.
|
|
17838
18068
|
Each execute spawns a new connection, so combining actions is 3-5x faster.
|
|
17839
18069
|
|
|
17840
|
-
2. Use
|
|
17841
|
-
- snapshot({format: 'compact'}) -> minimal ref:role:name output
|
|
18070
|
+
2. Use interactableOnly or limit depth for smaller snapshots (faster, fewer tokens).
|
|
17842
18071
|
- snapshot({interactableOnly: true}) -> only clickable/input elements
|
|
17843
18072
|
- snapshot({maxDepth: 5}) -> limit tree depth
|
|
17844
18073
|
|
|
@@ -17847,22 +18076,28 @@ PERFORMANCE TIPS
|
|
|
17847
18076
|
execute "await ref('e1').click()"
|
|
17848
18077
|
execute "return await snapshot()"
|
|
17849
18078
|
|
|
17850
|
-
# FAST: 1 round-trip,
|
|
18079
|
+
# FAST: 1 round-trip, interactable only
|
|
17851
18080
|
execute "
|
|
17852
18081
|
await page.goto('https://example.com');
|
|
17853
18082
|
await ref('e1').click();
|
|
17854
|
-
return await snapshot({
|
|
18083
|
+
return await snapshot({interactableOnly: true});
|
|
17855
18084
|
"
|
|
17856
18085
|
|
|
17857
18086
|
HELPERS
|
|
17858
18087
|
page - Playwright Page object
|
|
17859
18088
|
snapshot(opts?) - Get ARIA accessibility tree with refs
|
|
17860
18089
|
opts.maxDepth: limit tree depth (e.g., 5)
|
|
17861
|
-
opts.interactableOnly: only
|
|
17862
|
-
opts.format: "yaml" (default) or "compact"
|
|
18090
|
+
opts.interactableOnly: only clickable/input elements
|
|
17863
18091
|
ref(id) - Get element by ref ID (chainable - supports all ElementHandle methods)
|
|
17864
18092
|
Example: await ref('e1').click()
|
|
17865
18093
|
Example: await ref('e1').getAttribute('data-foo')
|
|
18094
|
+
ref(id).source() - Get React component source file info for element
|
|
18095
|
+
Returns { filePath, lineNumber, componentName } or null
|
|
18096
|
+
ref(id).props() - Get React component props (serialized)
|
|
18097
|
+
ref(id).state() - Get React component state/hooks (serialized)
|
|
18098
|
+
component(name, opts?) - Find elements by React component name
|
|
18099
|
+
opts.nth: get the nth matching element (0-indexed)
|
|
18100
|
+
Example: await component('Button', {nth: 0})
|
|
17866
18101
|
fill(id, text) - Clear and fill input (works with rich text editors)
|
|
17867
18102
|
drag(opts) - Drag with custom MIME types
|
|
17868
18103
|
opts.from: source selector or ref ID (e.g., "e1" or "text=src")
|
|
@@ -17878,21 +18113,16 @@ HELPERS
|
|
|
17878
18113
|
await waitFor('.btn') - wait for selector
|
|
17879
18114
|
await waitFor('networkidle') - wait for network idle
|
|
17880
18115
|
await waitFor('load') - wait for page load
|
|
17881
|
-
ref(id).source() - Get React component source file info for element
|
|
17882
|
-
Returns { filePath, lineNumber, componentName } or null
|
|
17883
18116
|
grab - React Grab client API (activate, copyElement, etc)
|
|
17884
18117
|
|
|
17885
|
-
SNAPSHOT
|
|
18118
|
+
SNAPSHOT OPTIONS
|
|
17886
18119
|
# Full YAML tree (default, can be large)
|
|
17887
18120
|
execute "return await snapshot()"
|
|
17888
18121
|
|
|
17889
18122
|
# Interactable only (recommended - much smaller!)
|
|
17890
18123
|
execute "return await snapshot({interactableOnly: true})"
|
|
17891
18124
|
|
|
17892
|
-
#
|
|
17893
|
-
execute "return await snapshot({format: 'compact'})"
|
|
17894
|
-
|
|
17895
|
-
# Combined options
|
|
18125
|
+
# With depth limit
|
|
17896
18126
|
execute "return await snapshot({interactableOnly: true, maxDepth: 6})"
|
|
17897
18127
|
|
|
17898
18128
|
SCREENSHOTS - PREFER ELEMENT OVER FULL PAGE
|
|
@@ -17936,15 +18166,28 @@ COMMON PATTERNS
|
|
|
17936
18166
|
dataTransfer: { 'application/x-custom': 'data' }
|
|
17937
18167
|
})"
|
|
17938
18168
|
|
|
17939
|
-
# Get React component source file
|
|
17940
|
-
execute "return await ref('e1').source()"
|
|
17941
|
-
|
|
17942
18169
|
# Get page info
|
|
17943
18170
|
execute "return {url: page.url(), title: await page.title()}"
|
|
17944
18171
|
|
|
17945
18172
|
# CSS selector fallback (refs are now in DOM as aria-ref)
|
|
17946
18173
|
execute "await page.click('[aria-ref="e1"]')"
|
|
17947
18174
|
|
|
18175
|
+
REACT-SPECIFIC PATTERNS
|
|
18176
|
+
# Get React component source file
|
|
18177
|
+
execute "return await ref('e1').source()"
|
|
18178
|
+
|
|
18179
|
+
# Get component props
|
|
18180
|
+
execute "return await ref('e1').props()"
|
|
18181
|
+
|
|
18182
|
+
# Get component state
|
|
18183
|
+
execute "return await ref('e1').state()"
|
|
18184
|
+
|
|
18185
|
+
# Find elements by React component name
|
|
18186
|
+
execute "const buttons = await component('Button'); return buttons.length"
|
|
18187
|
+
|
|
18188
|
+
# Get the first Button component and click it
|
|
18189
|
+
execute "const btn = await component('Button', {nth: 0}); await btn.click()"
|
|
18190
|
+
|
|
17948
18191
|
MULTI-PAGE SESSIONS
|
|
17949
18192
|
execute "await page.goto('https://github.com')" --page github
|
|
17950
18193
|
execute "return await snapshot({interactableOnly: true})" --page github
|
|
@@ -18001,14 +18244,7 @@ browser.addCommand(status);
|
|
|
18001
18244
|
browser.addCommand(execute);
|
|
18002
18245
|
browser.addCommand(pages);
|
|
18003
18246
|
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";
|
|
18247
|
+
var VERSION3 = "0.1.0-beta.4";
|
|
18012
18248
|
var isMac = process.platform === "darwin";
|
|
18013
18249
|
var META_LABEL = isMac ? "Cmd" : "Win";
|
|
18014
18250
|
var ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -18495,7 +18731,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
|
|
|
18495
18731
|
handleError(error48);
|
|
18496
18732
|
}
|
|
18497
18733
|
};
|
|
18498
|
-
var VERSION4 = "0.1.0";
|
|
18734
|
+
var VERSION4 = "0.1.0-beta.4";
|
|
18499
18735
|
var REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
18500
18736
|
var DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
18501
18737
|
var promptAgentIntegration = async (cwd, customPkg) => {
|
|
@@ -18539,19 +18775,29 @@ var promptAgentIntegration = async (cwd, customPkg) => {
|
|
|
18539
18775
|
}
|
|
18540
18776
|
}
|
|
18541
18777
|
if (integrationType === "skill" || integrationType === "both") {
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18778
|
+
const { skillTarget } = await prompts3({
|
|
18779
|
+
type: "select",
|
|
18780
|
+
name: "skillTarget",
|
|
18781
|
+
message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
|
|
18782
|
+
choices: SUPPORTED_TARGETS.map((target) => ({
|
|
18783
|
+
title: target,
|
|
18784
|
+
value: target
|
|
18785
|
+
}))
|
|
18786
|
+
});
|
|
18787
|
+
if (skillTarget) {
|
|
18552
18788
|
logger.break();
|
|
18553
|
-
skillSpinner
|
|
18554
|
-
|
|
18789
|
+
const skillSpinner = spinner("Installing browser automation skill").start();
|
|
18790
|
+
try {
|
|
18791
|
+
const skill = await fetchSkillFile();
|
|
18792
|
+
const skillDir = join(cwd, AGENT_TARGETS[skillTarget]);
|
|
18793
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
18794
|
+
mkdirSync(skillDir, { recursive: true });
|
|
18795
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill);
|
|
18796
|
+
skillSpinner.succeed(`Skill installed to ${AGENT_TARGETS[skillTarget]}/`);
|
|
18797
|
+
} catch {
|
|
18798
|
+
skillSpinner.fail("Failed to install skill");
|
|
18799
|
+
logger.dim("Try manually: npx -y openskills install aidenybai/react-grab");
|
|
18800
|
+
}
|
|
18555
18801
|
}
|
|
18556
18802
|
}
|
|
18557
18803
|
logger.break();
|
|
@@ -19187,7 +19433,7 @@ var init = new Command().name("init").description("initialize React Grab in your
|
|
|
19187
19433
|
await reportToCli("error", void 0, error48);
|
|
19188
19434
|
}
|
|
19189
19435
|
});
|
|
19190
|
-
var VERSION5 = "0.1.0";
|
|
19436
|
+
var VERSION5 = "0.1.0-beta.4";
|
|
19191
19437
|
var remove = new Command().name("remove").description("remove an agent integration").argument(
|
|
19192
19438
|
"[agent]",
|
|
19193
19439
|
"agent to remove (claude-code, cursor, opencode, codex, gemini, amp, ami, visual-edit)"
|
|
@@ -19366,7 +19612,7 @@ var remove = new Command().name("remove").description("remove an agent integrati
|
|
|
19366
19612
|
});
|
|
19367
19613
|
|
|
19368
19614
|
// src/cli.ts
|
|
19369
|
-
var VERSION6 = "0.1.0";
|
|
19615
|
+
var VERSION6 = "0.1.0-beta.4";
|
|
19370
19616
|
var VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
19371
19617
|
process.on("SIGINT", () => process.exit(0));
|
|
19372
19618
|
process.on("SIGTERM", () => process.exit(0));
|