@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.
Files changed (3) hide show
  1. package/dist/cli.cjs +381 -135
  2. package/dist/cli.js +381 -135
  3. 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
- const currentPage2 = getActivePage2();
3331
- return currentPage2.evaluate((el) => {
3332
- const g2 = globalThis;
3333
- if (!g2.__REACT_GRAB__) return null;
3334
- return g2.__REACT_GRAB__.getSource(el);
3335
- }, element);
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) => getElement(refId).then(
3350
- (el) => el.screenshot({ scale: "css", ...options2 })
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) => getElement(refId).then(
3354
- (el) => el[prop](...args)
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: async () => {
3464
- const currentPage2 = getActivePage2();
3465
- await currentPage2.evaluate(() => {
3466
- const g2 = globalThis;
3467
- g2.__REACT_GRAB__?.activate();
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
- const currentPage2 = getActivePage2();
3495
- return currentPage2.evaluate((el) => {
3496
- const g2 = globalThis;
3497
- return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
3498
- }, 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
- });
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 === "load" || selectorOrState === "domcontentloaded" || selectorOrState === "networkidle") {
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 { serverUrl } = await ensureHealthyServer();
17344
- const pageInfo = await getOrCreatePage(serverUrl, pageName);
17345
- browser2 = await playwrightCore.chromium.connectOverCDP(pageInfo.wsEndpoint);
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, format });
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, format}
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 { serverUrl } = await ensureHealthyServer();
17438
- const pageInfo = await getOrCreatePage(serverUrl, pageName);
17439
- browser2 = await playwrightCore.chromium.connectOverCDP(pageInfo.wsEndpoint);
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 && playwrightCookies.length > 0) {
17648
- await contexts[0].addCookies(playwrightCookies);
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 compact format or limit depth for smaller snapshots (faster, fewer tokens).
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, compact output
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({format: 'compact'});
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 show elements with refs
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 FORMATS
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
- # Compact format (minimal output: ref:role:name|ref:role:name)
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
- logger.break();
18553
- const skillSpinner = spinner("Installing browser automation skill").start();
18554
- try {
18555
- child_process.execSync(`npx -y openskills install aidenybai/react-grab -y`, {
18556
- stdio: "inherit",
18557
- cwd
18558
- });
18559
- logger.break();
18560
- skillSpinner.succeed("Skill installed to .claude/skills/");
18561
- } catch {
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.fail("Failed to install skill");
18564
- logger.dim("Try manually: npx -y openskills install aidenybai/react-grab");
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));