@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.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
- const currentPage2 = getActivePage2();
3321
- return currentPage2.evaluate((el) => {
3322
- const g2 = globalThis;
3323
- if (!g2.__REACT_GRAB__) return null;
3324
- return g2.__REACT_GRAB__.getSource(el);
3325
- }, element);
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) => getElement(refId).then(
3340
- (el) => el.screenshot({ scale: "css", ...options2 })
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) => getElement(refId).then(
3344
- (el) => el[prop](...args)
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: async () => {
3454
- const currentPage2 = getActivePage2();
3455
- await currentPage2.evaluate(() => {
3456
- const g2 = globalThis;
3457
- g2.__REACT_GRAB__?.activate();
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
- const currentPage2 = getActivePage2();
3485
- return currentPage2.evaluate((el) => {
3486
- const g2 = globalThis;
3487
- return g2.__REACT_GRAB__?.copyElement([el]) ?? false;
3488
- }, element);
3489
- },
3490
- getState: async () => {
3491
- const currentPage2 = getActivePage2();
3492
- return currentPage2.evaluate(() => {
3493
- const g2 = globalThis;
3494
- return g2.__REACT_GRAB__?.getState() ?? null;
3495
- });
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 === "load" || selectorOrState === "domcontentloaded" || selectorOrState === "networkidle") {
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 { serverUrl } = await ensureHealthyServer();
17334
- const pageInfo = await getOrCreatePage(serverUrl, pageName);
17335
- browser2 = await chromium.connectOverCDP(pageInfo.wsEndpoint);
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, format });
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, format}
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 { serverUrl } = await ensureHealthyServer();
17428
- const pageInfo = await getOrCreatePage(serverUrl, pageName);
17429
- browser2 = await chromium.connectOverCDP(pageInfo.wsEndpoint);
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 && playwrightCookies.length > 0) {
17638
- await contexts[0].addCookies(playwrightCookies);
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 compact format or limit depth for smaller snapshots (faster, fewer tokens).
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, compact output
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({format: 'compact'});
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 show elements with refs
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 FORMATS
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
- # Compact format (minimal output: ref:role:name|ref:role:name)
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
- logger.break();
18543
- const skillSpinner = spinner("Installing browser automation skill").start();
18544
- try {
18545
- execSync(`npx -y openskills install aidenybai/react-grab -y`, {
18546
- stdio: "inherit",
18547
- cwd
18548
- });
18549
- logger.break();
18550
- skillSpinner.succeed("Skill installed to .claude/skills/");
18551
- } catch {
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.fail("Failed to install skill");
18554
- logger.dim("Try manually: npx -y openskills install aidenybai/react-grab");
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));