Showing preview only (2,421K chars total). Download the full file or copy to clipboard to get everything.
Repository: aidenybai/react-grab
Branch: main
Commit: 30fc8d321fc5
Files: 499
Total size: 2.2 MB
Directory structure:
gitextract_xtlqk229/
├── .changeset/
│ ├── README.md
│ └── config.json
├── .github/
│ └── workflows/
│ ├── code-quality.yml
│ ├── publish-any-commit.yml
│ ├── pullfrog.yml
│ ├── test-build.yml
│ ├── test-cli.yml
│ └── test-e2e.yml
├── .gitignore
├── .oxfmtrc.json
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── packages/
│ ├── cli/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── commands/
│ │ │ │ ├── add.ts
│ │ │ │ ├── configure.ts
│ │ │ │ ├── init.ts
│ │ │ │ └── remove.ts
│ │ │ └── utils/
│ │ │ ├── cli-helpers.ts
│ │ │ ├── constants.ts
│ │ │ ├── detect.ts
│ │ │ ├── diff.ts
│ │ │ ├── handle-error.ts
│ │ │ ├── highlighter.ts
│ │ │ ├── install-mcp.ts
│ │ │ ├── install.ts
│ │ │ ├── is-non-interactive.ts
│ │ │ ├── logger.ts
│ │ │ ├── prompts.ts
│ │ │ ├── spinner.ts
│ │ │ ├── templates.ts
│ │ │ └── transform.ts
│ │ ├── test/
│ │ │ ├── configure.test.ts
│ │ │ ├── detect.test.ts
│ │ │ ├── diff.test.ts
│ │ │ ├── install-mcp.test.ts
│ │ │ ├── install.test.ts
│ │ │ ├── templates.test.ts
│ │ │ └── transform.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── design-system/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.tsx
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── e2e-playground/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ └── main.tsx
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ ├── grab/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── cli.js
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── build.js
│ │ └── tsconfig.json
│ ├── gym/
│ │ ├── .gitignore
│ │ ├── app/
│ │ │ ├── api/
│ │ │ │ └── provider/
│ │ │ │ └── [name]/
│ │ │ │ └── route.ts
│ │ │ ├── dashboard/
│ │ │ │ ├── data.json
│ │ │ │ └── page.tsx
│ │ │ ├── freeze-demo/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── playground/
│ │ │ └── page.tsx
│ │ ├── components/
│ │ │ ├── agent-playground.tsx
│ │ │ ├── app-sidebar.tsx
│ │ │ ├── chart-area-interactive.tsx
│ │ │ ├── counter.tsx
│ │ │ ├── data-table.tsx
│ │ │ ├── login-form.tsx
│ │ │ ├── nav-user.tsx
│ │ │ ├── search-form.tsx
│ │ │ ├── section-cards.tsx
│ │ │ ├── sheet-demo.tsx
│ │ │ ├── site-header.tsx
│ │ │ ├── theme-provider.tsx
│ │ │ ├── theme-toggle.tsx
│ │ │ ├── ui/
│ │ │ │ ├── avatar.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── chart.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── drawer.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── field.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── sidebar.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── toggle-group.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ └── version-switcher.tsx
│ │ ├── components.json
│ │ ├── hooks/
│ │ │ └── use-mobile.ts
│ │ ├── instrumentation-client.ts
│ │ ├── lib/
│ │ │ └── utils.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── scripts/
│ │ │ └── start-all-servers.js
│ │ └── tsconfig.json
│ ├── mcp/
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── constants.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-amp/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-claude-code/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-codex/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-copilot/
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-cursor/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-droid/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-gemini/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── provider-opencode/
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── cli.ts
│ │ │ ├── client.ts
│ │ │ ├── constants.ts
│ │ │ ├── handler.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── react-grab/
│ │ ├── .oxlintrc.json
│ │ ├── CHANGELOG.md
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── cli.js
│ │ ├── e2e/
│ │ │ ├── activation-key-config.spec.ts
│ │ │ ├── activation.spec.ts
│ │ │ ├── agent-integration.spec.ts
│ │ │ ├── agent-resume-race.spec.ts
│ │ │ ├── api-methods.spec.ts
│ │ │ ├── clear-history-prompt.spec.ts
│ │ │ ├── context-menu.spec.ts
│ │ │ ├── copy-feedback.spec.ts
│ │ │ ├── copy-styles.spec.ts
│ │ │ ├── disabled-elements.spec.ts
│ │ │ ├── drag-selection.spec.ts
│ │ │ ├── edge-cases.spec.ts
│ │ │ ├── element-context.spec.ts
│ │ │ ├── event-callbacks.spec.ts
│ │ │ ├── fixtures.ts
│ │ │ ├── focus-trap.spec.ts
│ │ │ ├── freeze-animations.spec.ts
│ │ │ ├── freeze-updates.spec.ts
│ │ │ ├── history-items.spec.ts
│ │ │ ├── history-reacquire.spec.ts
│ │ │ ├── hold-activation.spec.ts
│ │ │ ├── input-mode.spec.ts
│ │ │ ├── keyboard-navigation.spec.ts
│ │ │ ├── keyboard-shortcuts.spec.ts
│ │ │ ├── open-file.spec.ts
│ │ │ ├── overlay-filtering.spec.ts
│ │ │ ├── prompt-mode.spec.ts
│ │ │ ├── selection.spec.ts
│ │ │ ├── ssr.spec.ts
│ │ │ ├── theme-customization.spec.ts
│ │ │ ├── toggle-position-stability.spec.ts
│ │ │ ├── toolbar-menu.spec.ts
│ │ │ ├── toolbar-selection-hover.spec.ts
│ │ │ ├── toolbar.spec.ts
│ │ │ ├── touch-mode.spec.ts
│ │ │ ├── viewport.spec.ts
│ │ │ └── visual-feedback.spec.ts
│ │ ├── package.json
│ │ ├── playwright.config.ts
│ │ ├── scripts/
│ │ │ ├── css-rem-to-px.mjs
│ │ │ └── postinstall.cjs
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── clear-history-prompt.tsx
│ │ │ │ ├── context-menu.tsx
│ │ │ │ ├── history-dropdown.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── icon-check.tsx
│ │ │ │ │ ├── icon-chevron.tsx
│ │ │ │ │ ├── icon-clock.tsx
│ │ │ │ │ ├── icon-copy.tsx
│ │ │ │ │ ├── icon-ellipsis.tsx
│ │ │ │ │ ├── icon-loader.tsx
│ │ │ │ │ ├── icon-open.tsx
│ │ │ │ │ ├── icon-reply.tsx
│ │ │ │ │ ├── icon-retry.tsx
│ │ │ │ │ ├── icon-return.tsx
│ │ │ │ │ ├── icon-select.tsx
│ │ │ │ │ ├── icon-submit.tsx
│ │ │ │ │ └── icon-trash.tsx
│ │ │ │ ├── kbd.tsx
│ │ │ │ ├── overlay-canvas.tsx
│ │ │ │ ├── renderer.tsx
│ │ │ │ ├── selection-label/
│ │ │ │ │ ├── arrow-navigation-menu.tsx
│ │ │ │ │ ├── arrow.tsx
│ │ │ │ │ ├── bottom-section.tsx
│ │ │ │ │ ├── completion-view.tsx
│ │ │ │ │ ├── discard-prompt.tsx
│ │ │ │ │ ├── error-view.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── tag-badge.tsx
│ │ │ │ ├── toolbar/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── state.ts
│ │ │ │ │ ├── toolbar-content.tsx
│ │ │ │ │ └── toolbar-menu.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ ├── constants.ts
│ │ │ ├── core/
│ │ │ │ ├── agent/
│ │ │ │ │ ├── manager.ts
│ │ │ │ │ └── session.ts
│ │ │ │ ├── arrow-navigation.ts
│ │ │ │ ├── auto-scroll.ts
│ │ │ │ ├── context.ts
│ │ │ │ ├── copy.ts
│ │ │ │ ├── events.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── keyboard-handlers.ts
│ │ │ │ ├── log-intro.ts
│ │ │ │ ├── logo-svg.ts
│ │ │ │ ├── noop-api.ts
│ │ │ │ ├── plugin-registry.ts
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── comment.ts
│ │ │ │ │ ├── copy-html.ts
│ │ │ │ │ ├── copy-styles.ts
│ │ │ │ │ ├── copy.ts
│ │ │ │ │ ├── create-pending-selection-plugin.ts
│ │ │ │ │ └── open.ts
│ │ │ │ ├── store.ts
│ │ │ │ └── theme.ts
│ │ │ ├── index.ts
│ │ │ ├── primitives.ts
│ │ │ ├── styles.css
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── append-stack-context.ts
│ │ │ ├── auto-resize-textarea.ts
│ │ │ ├── clamp-to-viewport.ts
│ │ │ ├── cn.ts
│ │ │ ├── combine-bounds.ts
│ │ │ ├── confirmation-focus-manager.ts
│ │ │ ├── copy-content.ts
│ │ │ ├── create-anchored-dropdown.ts
│ │ │ ├── create-bounds-from-drag-rect.ts
│ │ │ ├── create-element-bounds.ts
│ │ │ ├── create-element-selector.ts
│ │ │ ├── create-menu-highlight.ts
│ │ │ ├── create-style-element.ts
│ │ │ ├── create-toolbar-drag.ts
│ │ │ ├── extract-element-css.ts
│ │ │ ├── format-relative-time.ts
│ │ │ ├── format-shortcut.ts
│ │ │ ├── freeze-animations.ts
│ │ │ ├── freeze-gsap.ts
│ │ │ ├── freeze-pseudo-states.ts
│ │ │ ├── freeze-updates.ts
│ │ │ ├── generate-id.ts
│ │ │ ├── generate-snippet.ts
│ │ │ ├── get-anchored-dropdown-position.ts
│ │ │ ├── get-arrow-size.ts
│ │ │ ├── get-bounds-center.ts
│ │ │ ├── get-element-at-position.ts
│ │ │ ├── get-element-center.ts
│ │ │ ├── get-elements-in-drag.ts
│ │ │ ├── get-next-base-path.ts
│ │ │ ├── get-script-options.ts
│ │ │ ├── get-tag-display.ts
│ │ │ ├── get-tag-name.ts
│ │ │ ├── get-visible-bounds-center.ts
│ │ │ ├── get-visual-viewport.ts
│ │ │ ├── history-storage.ts
│ │ │ ├── invalidate-interaction-caches.ts
│ │ │ ├── is-c-like-key.ts
│ │ │ ├── is-element-connected.ts
│ │ │ ├── is-element-visible.ts
│ │ │ ├── is-enter-code.ts
│ │ │ ├── is-event-from-overlay.ts
│ │ │ ├── is-extension-context.ts
│ │ │ ├── is-keyboard-event-triggered-by-input.ts
│ │ │ ├── is-mac.ts
│ │ │ ├── is-root-element.ts
│ │ │ ├── is-target-key-combination.ts
│ │ │ ├── is-valid-grabbable-element.ts
│ │ │ ├── join-snippets.ts
│ │ │ ├── key-matches-code.ts
│ │ │ ├── lerp.ts
│ │ │ ├── log-recoverable-error.ts
│ │ │ ├── mount-root.ts
│ │ │ ├── native-raf.ts
│ │ │ ├── normalize-error.ts
│ │ │ ├── on-idle.ts
│ │ │ ├── open-file.ts
│ │ │ ├── overlay-color.ts
│ │ │ ├── parse-activation-key.ts
│ │ │ ├── recalculate-session-position.ts
│ │ │ ├── register-overlay-dismiss.ts
│ │ │ ├── resolve-action-enabled.ts
│ │ │ ├── safe-polygon.ts
│ │ │ ├── strip-translate-from-transform.ts
│ │ │ ├── supports-display-p3.ts
│ │ │ ├── suppress-menu-event.ts
│ │ │ ├── toolbar-layout.ts
│ │ │ ├── toolbar-position.ts
│ │ │ └── truncate-string.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── relay/
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── client.ts
│ │ │ ├── connection.ts
│ │ │ ├── index.ts
│ │ │ ├── protocol.ts
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── shadcn-registry/
│ │ ├── package.json
│ │ ├── r/
│ │ │ └── react-grab.json
│ │ ├── registry/
│ │ │ └── react-grab.tsx
│ │ ├── registry.json
│ │ └── scripts/
│ │ └── build.js
│ ├── utils/
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ └── tsup.config.ts
│ ├── web-extension/
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── package.sh
│ │ ├── src/
│ │ │ ├── background/
│ │ │ │ └── service-worker.ts
│ │ │ ├── constants.ts
│ │ │ ├── content/
│ │ │ │ ├── bridge.ts
│ │ │ │ └── react-grab.ts
│ │ │ └── manifest.json
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── website/
│ ├── .gitignore
│ ├── .oxlintrc.json
│ ├── AGENTS.md
│ ├── app/
│ │ ├── api/
│ │ │ ├── og/
│ │ │ │ └── route.tsx
│ │ │ ├── report-cli/
│ │ │ │ └── route.ts
│ │ │ └── version/
│ │ │ └── route.ts
│ │ ├── blog/
│ │ │ ├── 1-0/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── agent/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── bets/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── intro/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── changelog/
│ │ │ └── page.tsx
│ │ ├── design-system/
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── not-found.tsx
│ │ ├── open-file/
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── privacy/
│ │ │ └── page.tsx
│ │ ├── robots.ts
│ │ └── sitemap.ts
│ ├── components/
│ │ ├── benchmark-tooltip.tsx
│ │ ├── benchmarks/
│ │ │ ├── benchmark-charts.tsx
│ │ │ ├── benchmark-detailed-table.tsx
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── blocks/
│ │ │ ├── grep-search-group.tsx
│ │ │ ├── grep-tool-call-block.tsx
│ │ │ ├── message-block.tsx
│ │ │ ├── read-tool-call-block.tsx
│ │ │ ├── streaming-text.tsx
│ │ │ └── thought-block.tsx
│ │ ├── blog-article-layout.tsx
│ │ ├── demo-footer.tsx
│ │ ├── github-button.tsx
│ │ ├── grab-element-button.tsx
│ │ ├── homepage-demo.tsx
│ │ ├── hotkey-context.tsx
│ │ ├── icons/
│ │ │ ├── icon-claude.tsx
│ │ │ ├── icon-codex.tsx
│ │ │ ├── icon-copilot.tsx
│ │ │ ├── icon-cursor.tsx
│ │ │ ├── icon-droid.tsx
│ │ │ ├── icon-github.tsx
│ │ │ ├── icon-nextjs.tsx
│ │ │ ├── icon-opencode.tsx
│ │ │ ├── icon-tanstack.tsx
│ │ │ ├── icon-vite.tsx
│ │ │ ├── icon-vscode.tsx
│ │ │ ├── icon-webstorm.tsx
│ │ │ └── icon-zed.tsx
│ │ ├── install-tabs.tsx
│ │ ├── mobile-demo-animation.tsx
│ │ ├── react-grab-logo.tsx
│ │ ├── table-of-contents.tsx
│ │ ├── ui/
│ │ │ ├── button.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── data-table-card.tsx
│ │ │ └── scrollable.tsx
│ │ ├── user-message.tsx
│ │ └── view-docs-button.tsx
│ ├── components.json
│ ├── constants.ts
│ ├── hooks/
│ │ └── use-stream.ts
│ ├── instrumentation-client.ts
│ ├── lib/
│ │ ├── api-helpers.ts
│ │ └── shiki.ts
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public/
│ │ ├── agent.webm
│ │ ├── demo.webm
│ │ ├── install.md
│ │ ├── llms.txt
│ │ ├── r/
│ │ │ ├── index.json
│ │ │ └── react-grab.json
│ │ ├── results.json
│ │ └── test-cases.json
│ ├── tsconfig.json
│ └── utils/
│ ├── cn.ts
│ ├── detect-mobile.ts
│ ├── get-key-from-code.ts
│ ├── hotkey-to-string.ts
│ └── parse-changelog.ts
├── pnpm-workspace.yaml
├── turbo.json
└── vercel.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [
[
"react-grab",
"grab",
"@react-grab/claude-code",
"@react-grab/cursor",
"@react-grab/opencode",
"@react-grab/codex",
"@react-grab/gemini",
"@react-grab/amp",
"@react-grab/droid",
"@react-grab/copilot",
"@react-grab/cli",
"@react-grab/utils",
"@react-grab/relay"
]
],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@react-grab/website",
"@react-grab/web-extension",
"@react-grab/gym",
"@react-grab/design-system"
]
}
================================================
FILE: .github/workflows/code-quality.yml
================================================
name: Code Quality
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Typecheck
run: pnpm typecheck
================================================
FILE: .github/workflows/publish-any-commit.yml
================================================
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- run: |
pnpm dlx pkg-pr-new publish \
./packages/react-grab \
./packages/cli \
./packages/grab \
./packages/relay \
./packages/utils \
./packages/provider-amp \
./packages/provider-claude-code \
./packages/provider-codex \
./packages/provider-cursor \
./packages/provider-copilot \
./packages/provider-droid \
./packages/provider-gemini \
./packages/provider-opencode
================================================
FILE: .github/workflows/pullfrog.yml
================================================
# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED
name: Pullfrog
run-name: ${{ inputs.name || github.workflow }}
on:
workflow_dispatch:
inputs:
prompt:
type: string
description: Agent prompt
name:
type: string
description: Run name
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
checks: read
jobs:
pullfrog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run agent
uses: pullfrog/pullfrog@v0
with:
prompt: ${{ inputs.prompt }}
env:
# add any additional keys your agent(s) need
# optionally, comment out any you won't use
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
================================================
FILE: .github/workflows/test-build.yml
================================================
name: Test Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test-build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
================================================
FILE: .github/workflows/test-cli.yml
================================================
name: Test CLI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test-cli:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Run CLI tests
run: pnpm --filter @react-grab/cli test
================================================
FILE: .github/workflows/test-e2e.yml
================================================
name: Test E2E
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test-e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm --filter react-grab exec playwright install chromium --with-deps
- name: Build
run: pnpm build
- name: Run E2E tests
run: pnpm --filter react-grab exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
.env
.turbo
dist
.next
**/*.tgz
coverage
react-grab-extension.zip
tsup.config.bundled_*.mjs
packages/website/public/script.js
test-results
playwright-report
.vercel
.cursor/debug.log
.cursor
meta.json
================================================
FILE: .oxfmtrc.json
================================================
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"tabWidth": 2,
"singleQuote": false,
"printWidth": 80,
"ignorePatterns": [
".next",
"node_modules",
"dist",
"build",
".turbo",
"pnpm-lock.yaml"
]
}
================================================
FILE: AGENTS.md
================================================
## General Rules
- MUST: Use @antfu/ni. Use `ni` to install, `nr SCRIPT_NAME` to run. `nun` to uninstall.
- MUST: Use TypeScript interfaces over types.
- MUST: Keep all types in the global scope.
- MUST: Use arrow functions over function declarations
- MUST: Never comment unless absolutely necessary.
- If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack
- Do not delete descriptive comments >3 lines without confirming with the user
- MUST: Use kebab-case for files
- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).
- Example: for .map(), you can use `innerX` instead of `x`
- Example: instead of `moved` use `didPositionChange`
- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
- MUST: Do not type cast ("as") unless absolutely necessary
- MUST: Remove unused code and don't repeat yourself.
- MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution.
- MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`).
- MUST: Put small, focused utility functions in `utils/` with one utility per file.
- MUST: Use Boolean over !!.
## SolidJS Rules
### Mental Model
- MUST: Treat components as setup functions that run ONCE, not render functions.
- MUST: Place reactive work in primitives (`createMemo`, `createEffect`, `<Show>`, `<For>`), not component body.
- MUST: Access signals only inside reactive contexts (JSX expressions, effects, memos).
### Reactivity
- MUST: Call signals as functions: `count()` not `count`.
- MUST: Use functional updates when new state depends on old: `setCount((prev) => prev + 1)`.
- MUST: Keep signals atomic (one per value) — one big state object loses granularity.
- MUST: Use derived functions `() => count() * 2` for cheap/infrequent derivations.
- MUST: Use `createMemo(() => ...)` for expensive/frequent derivations — caches result.
- MUST: Use `createEffect` for side effects only (DOM, localStorage, subscriptions).
- MUST: Call `onCleanup(() => ...)` inside effects for subscriptions/intervals/listeners.
- MUST: Use path syntax for store updates: `setStore("users", 0, "name", "Jane")`.
- MUST: Wrap store props in arrow for `on()`: `on(() => store.value, fn)` not `on(store.value, fn)`.
- SHOULD: Use `{ equals: false }` for trigger signals that always notify.
- SHOULD: Use `batch(() => { ... })` when updating multiple signals outside event handlers.
- SHOULD: Use `on(dep, fn)` for explicit effect dependencies.
- SHOULD: Use `untrack(() => value())` to read without subscribing.
- SHOULD: Use `createStore({ ... })` for nested objects with fine-grained reactivity.
- SHOULD: Use `produce(draft => { ... })` for complex store mutations.
- NEVER: Derive state via `createEffect(() => setX(y()))` — use memo or derived function.
- NEVER: Place side effects inside `createMemo` — causes infinite loops/crashes.
### Props
- MUST: Access props via `props.title`, not destructuring.
- SHOULD: Wrap in getter if needed: `const title = () => props.title`.
- SHOULD: Use `splitProps(props, ["keys"])` to separate local from pass-through props.
- SHOULD: Use `mergeProps(defaults, props)` for default values.
- SHOULD: Use `children(() => props.children)` only when transforming, otherwise `{props.children}`.
- NEVER: Destructure props `({ title })` — breaks reactivity.
### Control Flow
- MUST: Use `<For each={items()}>` for object arrays — item is value, index is signal.
- MUST: Use `<Index each={items()}>` for primitives/inputs — item is signal, index is number.
- MUST: Use `<Suspense fallback={...}>` for async, not `<Show when={!loading}>`.
- MUST: Access resource states via `data()`, `data.loading`, `data.error`, `data.latest`.
- SHOULD: Use `<Show when={cond()} fallback={...}>` for conditionals.
- SHOULD: Use `<Show when={val}>` callback for type narrowing: `{(v) => <div>{v().name}</div>}`.
- SHOULD: Use `<Switch>/<Match>` for multiple conditions.
- SHOULD: Use `createResource(source, fetcher)` for reactive async data.
- SHOULD: Use `<ErrorBoundary fallback={(err, reset) => ...}>` for render errors.
- NEVER: Use `.map()` in JSX — use `<For>` or `<Index>`.
- NEVER: Rely on ErrorBoundary for event handler or setTimeout errors — use try/catch.
### JSX & DOM
- MUST: Use `class` not `className`.
- MUST: Combine static `class="btn"` with reactive `classList={{ active: isActive() }}`.
- MUST: Use `onClick` for delegated events; `on:click` for native (element-level).
- MUST: Condition inside handler since events are not reactive: `onClick={() => props.onClick?.()}`.
- MUST: Read refs in `onMount` or effects — refs connect after render.
- MUST: Call `onCleanup` inside directives for cleanup.
- SHOULD: Use `on:click` for `stopPropagation`, capture, passive, or custom events.
- SHOULD: Use `style={{ color: color(), "--css-var": value() }}` for inline styles.
- SHOULD: Type refs as `let el: HTMLElement | undefined` with guard.
- SHOULD: Use `use:directiveName={accessor}` for reusable DOM behaviors.
- NEVER: Mix reactive `class={x()}` with `classList`.
## Testing
Run dev `packages/cli` with:
```bash
npm_command=exec node packages/cli/dist/cli.js
```
Run checks always before committing with:
```bash
pnpm test # runs e2e tests
pnpm lint
pnpm typecheck # runs type checking
pnpm format
```
## Development instructions
This is a pnpm + Turborepo monorepo (19 packages under `packages/`). No external services (databases, Docker, etc.) are required.
### Build before test
`pnpm build` must complete before `pnpm test` or `pnpm lint` — Turborepo `dependsOn` enforces this, but be aware that `pnpm test` will rebuild if the build cache is cold. After modifying source files, always rebuild before running tests.
### Approved build scripts
The root `package.json` has `pnpm.onlyBuiltDependencies` configured for `@parcel/watcher`, `esbuild`, `sharp`, `spawn-sync`, and `unrs-resolver`. Without this, `pnpm install` silently skips their native builds and downstream packages may fail.
### Playwright
E2E tests (`pnpm test` at root) run Playwright against the `e2e-playground` Vite dev server on port 5175 (auto-started by the Playwright config). Chromium must be installed: `npx --prefix packages/react-grab playwright install chromium --with-deps`.
### Key commands reference
See root `package.json` scripts and `CONTRIBUTING.md` for the full list. Quick reference:
- **Install**: `ni` (or `pnpm install`)
- **Build**: `nr build` (or `pnpm build`)
- **Dev watch**: `nr dev` (or `pnpm dev`) — watches core packages
- **Test**: `pnpm test` — runs Playwright E2E + Vitest CLI tests
- **Lint**: `pnpm lint` — oxlint on react-grab package
- **Typecheck**: `pnpm typecheck` — tsc on react-grab package
- **Format**: `pnpm format` — oxfmt
- **CLI dev**: `npm_command=exec node packages/cli/dist/cli.js`
- **E2E playground**: `pnpm --filter @react-grab/e2e-playground dev` (port 5175)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to React Grab
Thanks for your interest in contributing to React Grab! This document provides guidelines and instructions for contributing.
## Getting Started
### Prerequisites
- Node.js >= 18
- pnpm >= 8
### Setup
1. Fork and clone the repository:
```bash
git clone https://github.com/YOUR_USERNAME/react-grab.git
cd react-grab
```
2. Install dependencies using [@antfu/ni](https://github.com/antfu/ni):
```bash
ni
```
3. Build all packages:
```bash
nr build
```
4. Start development mode:
```bash
nr dev
```
## Project Structure
```
packages/
├── react-grab/ # Core library
├── grab/ # Bundled package (library + CLI, published as `grab`)
├── cli/ # CLI implementation (@react-grab/cli)
├── provider-cursor/ # Cursor agent integration
├── provider-claude-code/ # Claude Code integration
├── provider-opencode/ # OpenCode integration
├── provider-codex/ # OpenAI Codex integration
├── provider-gemini/ # Google Gemini CLI integration
├── provider-amp/ # Amp SDK integration
├── provider-droid/ # Droid integration
├── provider-copilot/ # Copilot integration
├── website/ # Documentation site (react-grab.com)
├── e2e-playground/ # E2E test target app
├── gym/ # Agent testing playground
└── web-extension/ # Browser extension
```
## Development Workflow
### Running the Gym
Test agent provider integrations in the gym:
```bash
pnpm --filter @react-grab/gym dev:claude # Claude Code
pnpm --filter @react-grab/gym dev:cursor # Cursor
pnpm --filter @react-grab/gym dev:opencode # OpenCode
pnpm --filter @react-grab/gym dev:codex # Codex
pnpm --filter @react-grab/gym dev:gemini # Gemini
pnpm --filter @react-grab/gym dev:amp # Amp
pnpm --filter @react-grab/gym dev:droid # Droid
pnpm --filter @react-grab/gym dev:copilot # Copilot
```
The gym runs at `http://localhost:5174` and lets you test react-grab's agent provider API with multiple backends.
### Running Tests
```bash
# Run CLI tests
pnpm --filter @react-grab/cli test
```
### Linting & Formatting
```bash
nr lint # Check for lint errors
nr lint:fix # Fix lint errors
nr format # Format code with oxfmt
```
## Code Style
- **Use TypeScript interfaces** over types
- **Use arrow functions** over function declarations
- **Use kebab-case** for file names
- **Use descriptive variable names** — avoid shorthands or 1-2 character names
- Example: `innerElement` instead of `el`
- Example: `didPositionChange` instead of `moved`
- **Avoid type casting** (`as`) unless absolutely necessary
- **Keep interfaces/types** at the global scope
- **Remove unused code** and follow DRY principles
- **Avoid comments** unless absolutely necessary
- If a hack is required, prefix with `// HACK: reason for hack`
## Submitting Changes
### Creating a Pull Request
1. Create a new branch:
```bash
git checkout -b feat/your-feature-name
```
2. Make your changes and commit with a descriptive message:
```bash
git commit -m "feat: add new feature"
```
3. Push to your fork and open a pull request
### Commit Convention
We use conventional commits:
- `feat:` — New feature
- `fix:` — Bug fix
- `docs:` — Documentation changes
- `chore:` — Maintenance tasks
- `refactor:` — Code refactoring
- `test:` — Test additions or changes
### Adding a Changeset
For changes that affect published packages, add a changeset:
```bash
nr changeset
```
Follow the prompts to describe your changes. This helps maintain accurate changelogs.
## Reporting Issues
Found a bug? Have a feature request? [Open an issue](https://github.com/aidenybai/react-grab/issues) with:
- Clear description of the problem or request
- Steps to reproduce (for bugs)
- Expected vs actual behavior
- Environment details (OS, browser, Node version)
## Community
- Join our [Discord](https://discord.com/invite/G7zxfUzkm7) to discuss ideas and get help
- Check existing [issues](https://github.com/aidenybai/react-grab/issues) before opening new ones
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Aiden Bai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# <img src="https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true" width="60" align="center" /> React Grab
[](https://bundlephobia.com/package/react-grab)
[](https://npmjs.com/package/react-grab)
[](https://npmjs.com/package/react-grab)
Select context for coding agents directly from your website
How? Point at any element and press **⌘C** (Mac) or **Ctrl+C** (Windows/Linux) to copy the file name, React component, and HTML source code.
It makes tools like Cursor, Claude Code, Copilot run up to [**3× faster**](https://react-grab.com/blog/intro) and more accurate.
### [Try out a demo! →](https://react-grab.com)

## Install
Run this command at your project root (where `next.config.ts` or `vite.config.ts` is located):
```bash
npx -y grab@latest init
```
## Connect to MCP
```bash
npx -y grab@latest add mcp
```
## Usage
Once installed, hover over any UI element in your browser and press:
- **⌘C** (Cmd+C) on Mac
- **Ctrl+C** on Windows/Linux
This copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example:
```js
<a class="ml-auto inline-block text-sm" href="#">
Forgot your password?
</a>
in LoginForm at components/login-form.tsx:46:19
```
## Manual Installation
If you're using a React framework or build tool, view instructions below:
#### Next.js (App router)
Add this inside of your `app/layout.tsx`:
```jsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html>
<head>
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
</head>
<body>{children}</body>
</html>
);
}
```
#### Next.js (Pages router)
Add this into your `pages/_document.tsx`:
```jsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head>
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
```
#### Vite
Add this at the top of your main entry file (e.g., `src/main.tsx`):
```tsx
if (import.meta.env.DEV) {
import("react-grab");
}
```
#### Webpack
First, install React Grab:
```bash
npm install react-grab
```
Then add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`):
```tsx
if (process.env.NODE_ENV === "development") {
import("react-grab");
}
```
## Plugins
Use plugins to extend React Grab's built-in UI with context menu actions, toolbar menu items, lifecycle hooks, and theme overrides. Plugins run within React Grab.
Register a plugin using the `registerPlugin` and `unregisterPlugin` exports:
```js
import { registerPlugin } from "react-grab";
registerPlugin({
name: "my-plugin",
hooks: {
onElementSelect: (element) => {
console.log("Selected:", element.tagName);
},
},
});
```
In React, register inside a `useEffect`:
```jsx
import { registerPlugin, unregisterPlugin } from "react-grab";
useEffect(() => {
registerPlugin({
name: "my-plugin",
actions: [
{
id: "my-action",
label: "My Action",
shortcut: "M",
onAction: (context) => {
console.log("Action on:", context.element);
context.hideContextMenu();
},
},
],
});
return () => unregisterPlugin("my-plugin");
}, []);
```
Actions use a `target` field to control where they appear. Omit `target` (or set `"context-menu"`) for the right-click menu, or set `"toolbar"` for the toolbar dropdown:
```js
actions: [
{
id: "inspect",
label: "Inspect",
shortcut: "I",
onAction: (ctx) => console.dir(ctx.element),
},
{
id: "toggle-freeze",
label: "Freeze",
target: "toolbar",
isActive: () => isFrozen,
onAction: () => toggleFreeze(),
},
];
```
See [`packages/react-grab/src/types.ts`](https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts) for the full `Plugin`, `PluginHooks`, and `PluginConfig` interfaces.
## Resources & Contributing Back
Want to try it out? Check out [our demo](https://react-grab.com).
Looking to contribute back? Check out the [Contributing Guide](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md).
Want to talk to the community? Hop in our [Discord](https://discord.com/invite/G7zxfUzkm7) and share your ideas and what you've built with React Grab.
Find a bug? Head over to our [issue tracker](https://github.com/aidenybai/react-grab/issues) and we'll do our best to help. We love pull requests, too!
We expect all contributors to abide by the terms of our [Code of Conduct](https://github.com/aidenybai/react-grab/blob/main/.github/CODE_OF_CONDUCT.md).
[**→ Start contributing on GitHub**](https://github.com/aidenybai/react-grab/blob/main/CONTRIBUTING.md)
### License
React Grab is MIT-licensed open-source software.
_Thank you to [Andrew Luetgers](https://github.com/andrewluetgers) for donating the `grab` npm package name._
================================================
FILE: package.json
================================================
{
"private": true,
"workspaces": [
"packages/*"
],
"type": "module",
"scripts": {
"build": "cp README.md packages/react-grab/README.md && turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/droid --filter=@react-grab/copilot --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/shadcn-registry && pnpm --filter grab build",
"dev": "turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/droid --filter=@react-grab/copilot --filter=@react-grab/cli --filter=@react-grab/utils",
"test": "turbo run test --filter=react-grab --filter=@react-grab/cli",
"typecheck": "pnpm --filter react-grab typecheck",
"lint": "pnpm --filter react-grab lint",
"lint:fix": "pnpm --filter react-grab lint:fix",
"format": "oxfmt",
"extension:dev": "pnpm --filter web-extension dev",
"extension:build": "pnpm --filter web-extension build",
"changeset": "changeset",
"version": "changeset version",
"release": "pnpm build && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"oxfmt": "^0.27.0",
"turbo": "^2.6.3",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"packageManager": "pnpm@10.24.0",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"sharp",
"spawn-sync",
"unrs-resolver"
]
}
}
================================================
FILE: packages/cli/CHANGELOG.md
================================================
# @react-grab/cli
## 0.1.28
### Patch Changes
- fix
## 0.1.27
### Patch Changes
- fix: install instructions
## 0.1.26
### Patch Changes
- fix: minor tweaks
## 0.1.25
### Patch Changes
- fix: primtiives
## 0.1.24
### Patch Changes
- primitives
## 0.1.23
### Patch Changes
- fix: npx command
## 0.1.22
### Patch Changes
- fix: freezing
## 0.1.21
### Patch Changes
- fix: up and down selection
## 0.1.20
### Patch Changes
- fix: selection performacne
## 0.1.19
### Patch Changes
- fix: gsap
## 0.1.18
### Patch Changes
- fix: minor issues
## 0.1.17
### Patch Changes
- fix: mcp
## 0.1.16
### Patch Changes
- fix: environment detection
## 0.1.15
### Patch Changes
- fix: animations and ux
## 0.1.14
### Patch Changes
- fix: improve recent UX
## 0.1.13
### Patch Changes
- fix MCP client injection
## 0.1.12
### Patch Changes
- feat: MCP
## 0.1.11
### Patch Changes
- fix: claude code provider
## 0.1.10
### Patch Changes
- feat: cdn in cli
## 0.1.9
### Patch Changes
- fix: startServer not exported in providers
## 0.1.8
### Patch Changes
- fix: providers not detaching
## 0.1.7
### Patch Changes
- fix: react freezing safety
## 0.1.6
### Patch Changes
- fix: compat with React Scan
## 0.1.5
### Patch Changes
- fix: fullscreen inset the root
## 0.1.4
### Patch Changes
- fix: improve cli edge cases
## 0.1.3
### Patch Changes
- fix: @react-grab/utils not publishing
## 0.1.2
### Patch Changes
- fix: packages not being published
## 0.1.1
### Patch Changes
- fix: clicking element after keyboard navigation
## 0.1.0
### Minor Changes
- 81adb50: feat: browser
### Patch Changes
- 81adb50: fix: shell script
- fb2b037: fix: cli
- a3d5a94: fix: cli global install
- 81adb50: feat: react support
- 81adb50: fix: a11y
- a5e7a6a: fix: optimize loading speed of cli
- 90af3f6: fix: CLI hanging
- 81adb50: fix: shell script
- 78efee2: fix: cli
- 074e593: fix: cli
- 5cd3709: fix: decouple browser out from react-grab
- 54c4867: ui improvements
## 0.1.0-beta.13
### Patch Changes
- ui improvements
## 0.1.0-beta.12
### Patch Changes
- fix: decouple browser out from react-grab
## 0.1.0-beta.11
### Patch Changes
- fix: cli global install
- Updated dependencies
- @react-grab/browser@0.1.0-beta.11
## 0.1.0-beta.10
### Patch Changes
- fix: cli
- Updated dependencies
- @react-grab/browser@0.1.0-beta.10
## 0.1.0-beta.9
### Patch Changes
- fix: cli
- Updated dependencies
- @react-grab/browser@0.1.0-beta.9
## 0.1.0-beta.8
### Patch Changes
- fix: cli
- Updated dependencies
- @react-grab/browser@0.1.0-beta.8
## 0.1.0-beta.7
### Patch Changes
- fix: CLI hanging
- Updated dependencies
- @react-grab/browser@0.1.0-beta.7
## 0.1.0-beta.6
### Patch Changes
- fix: optimize loading speed of cli
- Updated dependencies
- @react-grab/browser@0.1.0-beta.6
## 0.1.0-beta.5
### Patch Changes
- fix: a11y
- Updated dependencies
- @react-grab/browser@0.1.0-beta.5
## 0.1.0-beta.4
### Patch Changes
- feat: react support
- Updated dependencies
- @react-grab/browser@0.1.0-beta.4
## 0.1.0-beta.2
### Patch Changes
- fix: shell script
- Updated dependencies
- @react-grab/browser@0.1.0-beta.2
## 0.1.0-beta.1
### Patch Changes
- fix: shell script
- Updated dependencies
- @react-grab/browser@0.1.0-beta.1
## 0.1.0-beta.0
### Minor Changes
- feat: browser
### Patch Changes
- Updated dependencies
- @react-grab/browser@0.1.0-beta.0
## 0.0.98
### Patch Changes
- feat: new state architecture and context menu
## 0.0.97
### Patch Changes
- fix: sourcemap error
## 0.0.96
### Patch Changes
- fix: fiber access timeout handling
## 0.0.95
### Patch Changes
- fix: selecting buttons with disabled states
## 0.0.94
### Patch Changes
- fix: browser crashing on selection bug
## 0.0.93
### Patch Changes
- fix: copying not working
## 0.0.92
### Patch Changes
- refactor: use state machines instead of signals
## 0.0.91
### Patch Changes
- feat: dock
## 0.0.90
### Patch Changes
- fix: check visual edit endpoint
## 0.0.89
### Patch Changes
- fix: many bugfixes
## 0.0.88
### Patch Changes
- fix: deprecation errors
## 0.0.87
### Patch Changes
- feat: visual edits
## 0.0.86
### Patch Changes
- fix: editing
## 0.0.85
### Patch Changes
- fix: check versions on each provider
## 0.0.84
### Patch Changes
- fix: migrate from cross-spawn to execa to fix deprecation issue
## 0.0.83
### Patch Changes
- feat: timings during agent processing
## 0.0.82
### Patch Changes
- fix: agent support
## 0.0.81
### Patch Changes
- feat: codex and gemini support
## 0.0.80
### Patch Changes
- fix: replies and undo
## 0.0.79
### Patch Changes
- fix: claude code exit issue
## 0.0.78
### Patch Changes
- fix: cancel animation
## 0.0.77
### Patch Changes
- fix: new cli proxying
## 0.0.76
### Patch Changes
- feat: allow CLI under react-grab namespace
## 0.0.75
### Patch Changes
- fix: issue with Illegal Invocation on next.js pages
## 0.0.74
### Patch Changes
- fix: updateOptions
## 0.0.73
### Patch Changes
- fix: improve cli
## 0.0.72
### Patch Changes
- fix: shimmer effect
## 0.0.71
### Patch Changes
- fix: ux nits
## 0.0.70
### Patch Changes
- fix: react-grab cli flow when agents is used
## 0.0.69
### Patch Changes
- fix: CLI on script tag
## 0.0.68
### Patch Changes
- feat: opencode and cli installer
================================================
FILE: packages/cli/README.md
================================================
# @react-grab/cli
Interactive CLI to install React Grab in your project.
## Usage
```bash
npx grab
```
### Interactive Mode (default)
Running without options starts the interactive wizard:
```bash
npx grab
```
### Non-Interactive Mode
Pass options to skip prompts:
```bash
# Auto-detect everything and install without prompts
npx grab -y
# Specify framework
npx grab -f next -r app -y
# Use specific package manager
npx grab -p pnpm -y
```
## Options
| Option | Alias | Description | Choices |
| ------------------- | ----- | --------------------------------------------- | ---------------------------- |
| `--framework` | `-f` | Framework to configure | `next`, `vite`, `webpack` |
| `--package-manager` | `-p` | Package manager to use | `npm`, `yarn`, `pnpm`, `bun` |
| `--router` | `-r` | Next.js router type | `app`, `pages` |
| `--yes` | `-y` | Skip all confirmation prompts | - |
| `--skip-install` | - | Skip package installation (only modify files) | - |
| `--help` | `-h` | Show help | - |
| `--version` | `-v` | Show version | - |
## Examples
```bash
# Interactive setup
npx grab
# Quick install with auto-detection
npx grab -y
# Next.js App Router
npx grab -f next -r app -y
# Vite with pnpm
npx grab -f vite -p pnpm -y
# Only modify files (skip npm install)
npx grab --skip-install -y
```
## Supported Frameworks
| Framework | File Modified |
| ---------------------- | --------------------------------- |
| Next.js (App Router) | `app/layout.tsx` |
| Next.js (Pages Router) | `pages/_document.tsx` |
| Vite | `index.html` |
| Webpack | `src/index.tsx` or `src/main.tsx` |
## Manual Installation
If the CLI doesn't work for your setup, visit the docs:
https://react-grab.com/docs
================================================
FILE: packages/cli/package.json
================================================
{
"name": "@react-grab/cli",
"version": "0.1.28",
"bin": {
"react-grab": "./dist/cli.js"
},
"files": [
"dist"
],
"type": "module",
"exports": {
".": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js",
"require": "./dist/cli.cjs"
}
},
"scripts": {
"dev": "tsup --watch",
"build": "rm -rf dist && NODE_ENV=production tsup",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@antfu/ni": "^0.23.0",
"commander": "^14.0.0",
"ignore": "^7.0.5",
"jsonc-parser": "^3.3.1",
"ora": "^8.2.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"smol-toml": "^1.6.0"
},
"devDependencies": {
"@types/prompts": "^2.4.9",
"tsup": "^8.4.0",
"vitest": "^3.2.4"
}
}
================================================
FILE: packages/cli/src/cli.ts
================================================
import { Command } from "commander";
import { add } from "./commands/add.js";
import { configure } from "./commands/configure.js";
import { init } from "./commands/init.js";
import { remove } from "./commands/remove.js";
const VERSION = process.env.VERSION ?? "0.0.1";
const VERSION_API_URL = "https://www.react-grab.com/api/version";
process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));
try {
fetch(`${VERSION_API_URL}?source=cli&t=${Date.now()}`).catch(() => {});
} catch {}
const program = new Command()
.name("grab")
.description("add React Grab to your project")
.version(VERSION, "-v, --version", "display the version number");
program.addCommand(init);
program.addCommand(add);
program.addCommand(remove);
program.addCommand(configure);
const main = async () => {
await program.parseAsync();
};
main();
================================================
FILE: packages/cli/src/commands/add.ts
================================================
import { Command } from "commander";
import pc from "picocolors";
import { detectNonInteractive } from "../utils/is-non-interactive.js";
import { prompts } from "../utils/prompts.js";
import { detectProject } from "../utils/detect.js";
import { printDiff } from "../utils/diff.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import {
getPackagesToInstall,
getPackagesToUninstall,
installPackages,
uninstallPackages,
} from "../utils/install.js";
import {
installMcpServers,
promptConnectionMode,
promptMcpInstall,
} from "../utils/install-mcp.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";
import {
AGENTS,
AGENT_NAMES,
type Agent,
type AgentIntegration,
getAgentDisplayName,
} from "../utils/templates.js";
import {
applyPackageJsonTransform,
applyTransform,
previewAgentRemoval,
previewPackageJsonAgentRemoval,
previewPackageJsonTransform,
previewTransform,
} from "../utils/transform.js";
const VERSION = process.env.VERSION ?? "0.0.1";
const formatInstalledAgentNames = (agents: string[]): string =>
agents.map((agent) => AGENT_NAMES[agent as Agent] || agent).join(", ");
export const add = new Command()
.name("add")
.alias("install")
.description("connect React Grab to your agent")
.argument("[agent]", `agent to connect (${AGENTS.join(", ")}, mcp)`)
.option("-y, --yes", "skip confirmation prompts", false)
.option(
"-c, --cwd <cwd>",
"working directory (defaults to current directory)",
process.cwd(),
)
.action(async (agentArg, opts) => {
console.log(
`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION)}`,
);
console.log();
try {
const cwd = opts.cwd;
const isNonInteractive = detectNonInteractive(opts.yes);
const preflightSpinner = spinner("Preflight checks.").start();
const projectInfo = await detectProject(cwd);
if (!projectInfo.hasReactGrab) {
preflightSpinner.fail("React Grab is not installed.");
logger.break();
logger.error(
`Run ${highlighter.info("react-grab init")} first to install React Grab.`,
);
logger.break();
process.exit(1);
}
preflightSpinner.succeed();
const availableAgents = AGENTS.filter(
(agent) => !projectInfo.installedAgents.includes(agent),
);
if (availableAgents.length === 0 && isNonInteractive && !agentArg) {
logger.break();
logger.success("All legacy agents are already installed.");
logger.log("Run without -y to add MCP.");
logger.break();
process.exit(0);
}
let agentIntegration: AgentIntegration;
let agentsToRemove: Agent[] = [];
if (agentArg === "mcp") {
if (isNonInteractive) {
const results = installMcpServers();
const hasSuccess = results.some((result) => result.success);
if (!hasSuccess) {
logger.break();
logger.error("Failed to install MCP server.");
logger.break();
process.exit(1);
}
} else {
const didInstall = await promptMcpInstall();
if (!didInstall) {
logger.break();
process.exit(0);
}
}
logger.break();
logger.log(
`${highlighter.success("Success!")} MCP server has been configured.`,
);
logger.log("Restart your agents to activate.");
logger.break();
agentIntegration = "mcp";
projectInfo.installedAgents = [...projectInfo.installedAgents, "mcp"];
} else if (agentArg) {
if (!AGENTS.includes(agentArg as (typeof AGENTS)[number])) {
logger.break();
logger.error(`Invalid agent: ${agentArg}`);
logger.error(`Available agents: ${AGENTS.join(", ")}, mcp`);
logger.break();
process.exit(1);
}
const validAgent = agentArg as Agent;
if (projectInfo.installedAgents.includes(validAgent)) {
logger.break();
logger.warn(`${AGENT_NAMES[validAgent]} is already installed.`);
logger.break();
process.exit(0);
}
agentIntegration = validAgent;
if (projectInfo.installedAgents.length > 0 && !isNonInteractive) {
const installedNames = formatInstalledAgentNames(
projectInfo.installedAgents,
);
logger.break();
logger.warn(`${installedNames} is already installed.`);
const { action } = await prompts({
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: [
{
title: `Replace with ${getAgentDisplayName(agentIntegration)}`,
value: "replace",
},
{
title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,
value: "add",
},
{ title: "Cancel", value: "cancel" },
],
});
if (!action || action === "cancel") {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
if (action === "replace") {
agentsToRemove = [...projectInfo.installedAgents] as Agent[];
}
}
} else if (!isNonInteractive) {
if (projectInfo.installedAgents.length > 0) {
const installedNames = formatInstalledAgentNames(
projectInfo.installedAgents,
);
logger.warn(`Currently installed: ${installedNames}`);
logger.break();
}
const connectionMode = await promptConnectionMode();
if (connectionMode === undefined) {
logger.break();
process.exit(1);
}
if (connectionMode === "mcp") {
const didInstall = await promptMcpInstall();
if (!didInstall) {
logger.break();
process.exit(0);
}
logger.break();
logger.log(
`${highlighter.success("Success!")} MCP server has been configured.`,
);
logger.log("Restart your agents to activate.");
logger.break();
agentIntegration = "mcp";
projectInfo.installedAgents = [...projectInfo.installedAgents, "mcp"];
} else {
const { agent } = await prompts({
type: "select",
name: "agent",
message: `Which ${highlighter.info("agent")} would you like to connect?`,
choices: [
...availableAgents.map((availableAgent) => ({
title: AGENT_NAMES[availableAgent],
value: availableAgent,
})),
{ title: "Skip", value: "skip" },
],
});
if (!agent || agent === "skip") {
logger.break();
process.exit(0);
}
agentIntegration = agent as AgentIntegration;
if (projectInfo.installedAgents.length > 0) {
const installedNames = formatInstalledAgentNames(
projectInfo.installedAgents,
);
const { action } = await prompts({
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: [
{
title: `Replace ${installedNames} with ${getAgentDisplayName(agentIntegration)}`,
value: "replace",
},
{
title: `Add ${getAgentDisplayName(agentIntegration)} alongside existing`,
value: "add",
},
{ title: "Cancel", value: "cancel" },
],
});
if (!action || action === "cancel") {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
if (action === "replace") {
agentsToRemove = [...projectInfo.installedAgents] as Agent[];
}
}
}
} else {
logger.break();
logger.error("Please specify an agent to connect.");
logger.error("Available agents: " + availableAgents.join(", "));
logger.break();
process.exit(1);
}
if (agentsToRemove.length > 0) {
for (const agentToRemove of agentsToRemove) {
const removalResult = previewAgentRemoval(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentToRemove,
);
const removalPackageJsonResult = previewPackageJsonAgentRemoval(
projectInfo.projectRoot,
agentToRemove,
);
const packagesToRemove = getPackagesToUninstall(agentToRemove);
if (packagesToRemove.length > 0) {
const uninstallSpinner = spinner(
`Removing ${packagesToRemove.join(", ")}.`,
).start();
try {
uninstallPackages(
packagesToRemove,
projectInfo.packageManager,
projectInfo.projectRoot,
);
uninstallSpinner.succeed();
} catch (error) {
uninstallSpinner.fail();
handleError(error);
}
}
if (
removalResult.success &&
!removalResult.noChanges &&
removalResult.newContent
) {
const removeWriteSpinner = spinner(
`Removing ${AGENT_NAMES[agentToRemove]} from ${removalResult.filePath}.`,
).start();
const writeResult = applyTransform(removalResult);
if (!writeResult.success) {
removeWriteSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
removeWriteSpinner.succeed();
}
if (
removalPackageJsonResult.success &&
!removalPackageJsonResult.noChanges &&
removalPackageJsonResult.newContent
) {
const removePackageJsonSpinner = spinner(
`Removing ${AGENT_NAMES[agentToRemove]} from ${removalPackageJsonResult.filePath}.`,
).start();
const packageJsonWriteResult = applyPackageJsonTransform(
removalPackageJsonResult,
);
if (!packageJsonWriteResult.success) {
removePackageJsonSpinner.fail();
logger.break();
logger.error(
packageJsonWriteResult.error || "Failed to write file.",
);
logger.break();
process.exit(1);
}
removePackageJsonSpinner.succeed();
}
}
projectInfo.installedAgents = projectInfo.installedAgents.filter(
(installedAgent) => !agentsToRemove.includes(installedAgent as Agent),
);
}
const addingSpinner = spinner(
`Adding ${getAgentDisplayName(agentIntegration)}.`,
).start();
addingSpinner.succeed();
const result = previewTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentIntegration,
true,
);
const packageJsonResult = previewPackageJsonTransform(
projectInfo.projectRoot,
agentIntegration,
projectInfo.installedAgents,
projectInfo.packageManager,
);
if (!result.success) {
logger.break();
logger.error(result.message);
logger.break();
process.exit(1);
}
const hasLayoutChanges =
!result.noChanges && result.originalContent && result.newContent;
const hasPackageJsonChanges =
packageJsonResult.success &&
!packageJsonResult.noChanges &&
packageJsonResult.originalContent &&
packageJsonResult.newContent;
if (hasLayoutChanges || hasPackageJsonChanges) {
logger.break();
if (hasLayoutChanges) {
printDiff(
result.filePath,
result.originalContent!,
result.newContent!,
);
}
if (hasPackageJsonChanges) {
if (hasLayoutChanges) {
logger.break();
}
printDiff(
packageJsonResult.filePath,
packageJsonResult.originalContent!,
packageJsonResult.newContent!,
);
}
if (!isNonInteractive && agentsToRemove.length === 0) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
}
}
const packages = getPackagesToInstall(agentIntegration, false);
if (packages.length > 0) {
const installSpinner = spinner(
`Installing ${packages.join(", ")}.`,
).start();
try {
installPackages(
packages,
projectInfo.packageManager,
projectInfo.projectRoot,
);
installSpinner.succeed();
} catch (error) {
installSpinner.fail();
handleError(error);
}
}
if (hasLayoutChanges) {
const writeSpinner = spinner(
`Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
}
if (hasPackageJsonChanges) {
const packageJsonSpinner = spinner(
`Applying changes to ${packageJsonResult.filePath}.`,
).start();
const packageJsonWriteResult =
applyPackageJsonTransform(packageJsonResult);
if (!packageJsonWriteResult.success) {
packageJsonSpinner.fail();
logger.break();
logger.error(packageJsonWriteResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
packageJsonSpinner.succeed();
}
logger.break();
logger.log(
`${highlighter.success("Success!")} ${getAgentDisplayName(agentIntegration)} has been added.`,
);
if (packageJsonResult.warning) {
logger.warn(packageJsonResult.warning);
} else {
logger.log("Make sure to start the agent server before using it.");
}
logger.break();
} catch (error) {
handleError(error);
}
});
================================================
FILE: packages/cli/src/commands/configure.ts
================================================
import { Command } from "commander";
import pc from "picocolors";
import { prompts } from "../utils/prompts.js";
import { detectProject } from "../utils/detect.js";
import { printDiff } from "../utils/diff.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";
import {
applyOptionsTransform,
applyTransform,
previewCdnTransform,
previewOptionsTransform,
type ReactGrabOptions,
} from "../utils/transform.js";
import {
MAX_SUGGESTIONS_COUNT,
MAX_KEY_HOLD_DURATION_MS,
MAX_CONTEXT_LINES,
} from "../utils/constants.js";
const VERSION = process.env.VERSION ?? "0.0.1";
interface ConfigOption {
id: string;
title: string;
description: string;
}
const isMac = process.platform === "darwin";
const META_LABEL = isMac ? "Cmd" : "Win";
const ALT_LABEL = isMac ? "Option" : "Alt";
const MODIFIER_ALIASES: Record<string, string> = {
cmd: "meta",
command: "meta",
win: "meta",
windows: "meta",
meta: "meta",
ctrl: "ctrl",
control: "ctrl",
shift: "shift",
alt: "alt",
option: "alt",
opt: "alt",
};
const MODIFIERS = ["meta", "ctrl", "shift", "alt"] as const;
const BASE_KEYS: Array<{ key: string; aliases: string[] }> = [
{ key: " ", aliases: ["space", "spacebar"] },
{ key: "Enter", aliases: ["enter", "return"] },
{ key: "Escape", aliases: ["escape", "esc"] },
{ key: "Tab", aliases: ["tab"] },
{ key: "Backspace", aliases: ["backspace", "back"] },
{ key: "Delete", aliases: ["delete", "del"] },
{ key: "Insert", aliases: ["insert", "ins"] },
{ key: "Home", aliases: ["home"] },
{ key: "End", aliases: ["end"] },
{ key: "PageUp", aliases: ["pageup", "pgup"] },
{ key: "PageDown", aliases: ["pagedown", "pgdn", "pgdown"] },
{ key: "ArrowUp", aliases: ["arrowup", "up"] },
{ key: "ArrowDown", aliases: ["arrowdown", "down"] },
{ key: "ArrowLeft", aliases: ["arrowleft", "left"] },
{ key: "ArrowRight", aliases: ["arrowright", "right"] },
...Array.from({ length: 12 }, (_, i) => ({
key: `F${i + 1}`,
aliases: [`f${i + 1}`],
})),
...Array.from({ length: 26 }, (_, i) => {
const letter = String.fromCharCode(97 + i);
return { key: letter, aliases: [letter] };
}),
...Array.from({ length: 10 }, (_, i) => ({
key: String(i),
aliases: [String(i)],
})),
{ key: "`", aliases: ["backtick", "grave", "`"] },
{ key: "-", aliases: ["minus", "dash", "-"] },
{ key: "=", aliases: ["equals", "equal", "="] },
{ key: "[", aliases: ["leftbracket", "lbracket", "["] },
{ key: "]", aliases: ["rightbracket", "rbracket", "]"] },
{ key: "\\", aliases: ["backslash", "\\"] },
{ key: ";", aliases: ["semicolon", ";"] },
{ key: "'", aliases: ["quote", "apostrophe", "'"] },
{ key: ",", aliases: ["comma", ","] },
{ key: ".", aliases: ["period", "dot", "."] },
{ key: "/", aliases: ["slash", "forwardslash", "/"] },
];
interface KeyCombo {
key: string;
metaKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
}
interface KeyChoice {
title: string;
value: KeyCombo;
}
const formatCombo = (combo: KeyCombo): string => {
const parts: string[] = [];
if (combo.metaKey) parts.push(META_LABEL);
if (combo.ctrlKey) parts.push("Ctrl");
if (combo.shiftKey) parts.push("Shift");
if (combo.altKey) parts.push(ALT_LABEL);
const keyDisplay =
combo.key === " "
? "Space"
: combo.key.length === 1
? combo.key.toUpperCase()
: combo.key;
parts.push(keyDisplay);
return parts.join("+");
};
const parseInput = (
input: string,
): { modifiers: Set<string>; partial: string } => {
const normalized = input.toLowerCase().replace(/\s+/g, "");
const parts = normalized.split(/[+\-]/);
const modifiers = new Set<string>();
let partial = "";
for (const part of parts) {
if (!part) continue;
const modifierKey = MODIFIER_ALIASES[part];
if (modifierKey) {
modifiers.add(modifierKey);
} else {
partial = part;
}
}
return { modifiers, partial };
};
const POPULAR_KEYS = ["g", "k", "e", "d", "b", " ", "Escape", "Enter"];
const generateSuggestions = (input: string): KeyChoice[] => {
const { modifiers, partial } = parseInput(input);
const suggestions: KeyChoice[] = [];
if (!partial && modifiers.size === 0 && !input) {
for (const mod of MODIFIERS) {
const label =
mod === "meta"
? META_LABEL
: mod === "alt"
? ALT_LABEL
: mod.charAt(0).toUpperCase() + mod.slice(1);
for (const popularKey of POPULAR_KEYS) {
const keyDisplay =
popularKey === " "
? "Space"
: popularKey.length === 1
? popularKey.toUpperCase()
: popularKey;
suggestions.push({
title: `${label}+${keyDisplay}`,
value: {
key: popularKey,
...(mod === "meta" ? { metaKey: true } : {}),
...(mod === "ctrl" ? { ctrlKey: true } : {}),
...(mod === "shift" ? { shiftKey: true } : {}),
...(mod === "alt" ? { altKey: true } : {}),
},
});
}
}
for (const baseKey of BASE_KEYS) {
suggestions.push({
title:
baseKey.key === " "
? "Space"
: baseKey.key.length === 1
? baseKey.key.toUpperCase()
: baseKey.key,
value: { key: baseKey.key },
});
}
return suggestions;
}
const buildCombo = (
key: string,
mods: Set<string>,
extraMod?: string,
): KeyCombo => ({
key,
...(mods.has("meta") || extraMod === "meta" ? { metaKey: true } : {}),
...(mods.has("ctrl") || extraMod === "ctrl" ? { ctrlKey: true } : {}),
...(mods.has("shift") || extraMod === "shift" ? { shiftKey: true } : {}),
...(mods.has("alt") || extraMod === "alt" ? { altKey: true } : {}),
});
for (const baseKey of BASE_KEYS) {
const matches = partial
? baseKey.aliases.some((alias) => alias.startsWith(partial))
: true;
if (matches) {
const combo = buildCombo(baseKey.key, modifiers);
suggestions.push({
title: formatCombo(combo),
value: combo,
});
}
}
if (!partial) {
const unusedMods = MODIFIERS.filter((m) => !modifiers.has(m));
for (const mod of unusedMods) {
for (const popularKey of POPULAR_KEYS) {
const combo = buildCombo(popularKey, modifiers, mod);
suggestions.push({
title: formatCombo(combo),
value: combo,
});
}
}
}
return suggestions.slice(0, MAX_SUGGESTIONS_COUNT);
};
const CONFIG_OPTIONS: ConfigOption[] = [
{
id: "activationKey",
title: "Activation Key",
description: "The key used to activate React Grab (e.g., g, k, space)",
},
{
id: "activationMode",
title: "Activation Mode",
description: "Toggle (press to activate/deactivate) or Hold (hold key)",
},
{
id: "keyHoldDuration",
title: "Key Hold Duration",
description: "Milliseconds to hold the key before activation (hold mode)",
},
{
id: "allowActivationInsideInput",
title: "Allow Activation Inside Input",
description: "Whether to allow activation when focused on input fields",
},
{
id: "maxContextLines",
title: "Max Context Lines",
description: "Number of surrounding code lines to include in context",
},
];
const formatActivationKeyDisplay = (
activationKey: ReactGrabOptions["activationKey"],
): string => {
if (!activationKey) return "Default (Option/Alt)";
return activationKey
.split("+")
.map((part) => {
const lower = part.toLowerCase();
if (lower === "meta") return process.platform === "darwin" ? "⌘" : "Win";
if (lower === "alt") return process.platform === "darwin" ? "⌥" : "Alt";
if (lower === "ctrl") return "Ctrl";
if (lower === "shift") return "Shift";
if (lower === "space" || lower === " ") return "Space";
return part.toUpperCase();
})
.join(" + ");
};
const comboToString = (combo: KeyCombo): string => {
const parts: string[] = [];
if (combo.metaKey) parts.push("Meta");
if (combo.ctrlKey) parts.push("Ctrl");
if (combo.shiftKey) parts.push("Shift");
if (combo.altKey) parts.push("Alt");
if (combo.key) {
const keyDisplay = combo.key === " " ? "Space" : combo.key;
parts.push(keyDisplay);
}
return parts.join("+");
};
export const configure = new Command()
.name("configure")
.alias("config")
.description("configure React Grab options")
.option("-y, --yes", "skip confirmation prompts", false)
.option(
"-k, --key <key>",
"activation key (e.g., Meta+K, Ctrl+Shift+G, Space)",
)
.option("-m, --mode <mode>", "activation mode (toggle, hold)")
.option(
"--hold-duration <ms>",
"key hold duration in milliseconds (for hold mode)",
)
.option(
"--allow-input <boolean>",
"allow activation inside input fields (true/false)",
)
.option("--context-lines <lines>", "max context lines to include")
.option(
"--cdn <domain>",
"CDN domain (e.g., unpkg.com, custom.react-grab.com)",
)
.option(
"-c, --cwd <cwd>",
"working directory (defaults to current directory)",
process.cwd(),
)
.action(async (opts) => {
console.log(
`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION)}`,
);
console.log();
try {
const cwd = opts.cwd;
const preflightSpinner = spinner("Preflight checks.").start();
const projectInfo = await detectProject(cwd);
if (!projectInfo.hasReactGrab) {
preflightSpinner.fail("React Grab is not installed.");
logger.break();
logger.error(
`Run ${highlighter.info("react-grab init")} first to install React Grab.`,
);
logger.break();
process.exit(1);
}
preflightSpinner.succeed();
if (opts.cdn) {
const result = previewCdnTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
opts.cdn,
);
if (!result.success) {
logger.break();
logger.error(result.message);
logger.break();
process.exit(1);
}
if (result.noChanges) {
logger.break();
logger.log("No changes needed.");
logger.break();
process.exit(0);
}
logger.break();
printDiff(result.filePath, result.originalContent!, result.newContent!);
if (!opts.yes) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
}
const writeSpinner = spinner(
`Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
logger.break();
logger.log(`${highlighter.success("Success!")} CDN updated.`);
logger.break();
return;
}
const hasFlags =
opts.key ||
opts.mode ||
opts.holdDuration ||
opts.allowInput ||
opts.contextLines;
logger.break();
logger.log(`Configure ${highlighter.info("React Grab")} options:`);
logger.break();
const collectedOptions: ReactGrabOptions = {};
if (hasFlags) {
if (opts.key) {
collectedOptions.activationKey = opts.key;
logger.log(
` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,
);
}
if (opts.mode) {
if (opts.mode !== "toggle" && opts.mode !== "hold") {
logger.error(`Invalid mode: ${opts.mode}. Use "toggle" or "hold".`);
logger.break();
process.exit(1);
}
collectedOptions.activationMode = opts.mode;
logger.log(` Activation mode: ${highlighter.info(opts.mode)}`);
}
if (opts.holdDuration) {
const duration = parseInt(opts.holdDuration, 10);
if (
isNaN(duration) ||
duration < 0 ||
duration > MAX_KEY_HOLD_DURATION_MS
) {
logger.error(
`Invalid hold duration. Must be 0-${MAX_KEY_HOLD_DURATION_MS}ms.`,
);
logger.break();
process.exit(1);
}
collectedOptions.keyHoldDuration = duration;
logger.log(
` Key hold duration: ${highlighter.info(`${duration}ms`)}`,
);
}
if (opts.allowInput !== undefined) {
const allowInput =
opts.allowInput === "true" || opts.allowInput === true;
collectedOptions.allowActivationInsideInput = allowInput;
logger.log(
` Allow activation inside input: ${highlighter.info(String(allowInput))}`,
);
}
if (opts.contextLines) {
const lines = parseInt(opts.contextLines, 10);
if (isNaN(lines) || lines < 0 || lines > MAX_CONTEXT_LINES) {
logger.error(
`Invalid context lines. Must be 0-${MAX_CONTEXT_LINES}.`,
);
logger.break();
process.exit(1);
}
collectedOptions.maxContextLines = lines;
logger.log(` Max context lines: ${highlighter.info(String(lines))}`);
}
} else {
const { selectedOption } = await prompts({
type: "autocomplete",
name: "selectedOption",
message: "Search for an option to configure:",
choices: CONFIG_OPTIONS.map((option) => ({
title: option.title,
value: option.id,
description: option.description,
})),
suggest: (input, choices) =>
Promise.resolve(
choices.filter(
(choice) =>
choice.title.toLowerCase().includes(input.toLowerCase()) ||
(choice.description
?.toLowerCase()
.includes(input.toLowerCase()) ??
false),
),
),
});
if (selectedOption === undefined) {
logger.break();
process.exit(1);
}
if (selectedOption === "activationKey") {
const { selectedCombo } = await prompts({
type: "autocomplete",
name: "selectedCombo",
message: "Type key combination (e.g. ctrl+shift+g):",
choices: generateSuggestions(""),
suggest: (input) => Promise.resolve(generateSuggestions(input)),
});
if (selectedCombo === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.activationKey = comboToString(selectedCombo);
logger.log(
` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,
);
}
if (selectedOption === "activationMode") {
const { activationMode } = await prompts({
type: "select",
name: "activationMode",
message: `Select ${highlighter.info("activation mode")}:`,
choices: [
{
title: "Toggle (press to activate/deactivate)",
value: "toggle",
},
{ title: "Hold (hold key to keep active)", value: "hold" },
],
initial: 0,
});
if (activationMode === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.activationMode = activationMode;
}
if (selectedOption === "keyHoldDuration") {
const { keyHoldDuration } = await prompts({
type: "number",
name: "keyHoldDuration",
message: `Enter ${highlighter.info("key hold duration")} in milliseconds:`,
initial: 150,
min: 0,
max: 2000,
});
if (keyHoldDuration === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.keyHoldDuration = keyHoldDuration;
}
if (selectedOption === "allowActivationInsideInput") {
const { allowActivationInsideInput } = await prompts({
type: "confirm",
name: "allowActivationInsideInput",
message: `Allow activation ${highlighter.info("inside input fields")}?`,
initial: true,
});
if (allowActivationInsideInput === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.allowActivationInsideInput =
allowActivationInsideInput;
}
if (selectedOption === "maxContextLines") {
const { maxContextLines } = await prompts({
type: "number",
name: "maxContextLines",
message: `Enter ${highlighter.info("max context lines")} to include:`,
initial: 3,
min: 0,
max: 50,
});
if (maxContextLines === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.maxContextLines = maxContextLines;
}
}
const result = previewOptionsTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
collectedOptions,
);
if (!result.success) {
logger.break();
logger.warn(result.message);
logger.break();
const configJson = JSON.stringify(collectedOptions);
logger.log(
`Add this to your ${highlighter.info("init()")} call or ${highlighter.info("data-options")} attribute:`,
);
logger.break();
console.log(` ${pc.cyan(configJson)}`);
logger.break();
process.exit(1);
}
const hasChanges =
!result.noChanges && result.originalContent && result.newContent;
if (hasChanges) {
logger.break();
printDiff(result.filePath, result.originalContent!, result.newContent!);
if (!opts.yes) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
}
const writeSpinner = spinner(
`Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyOptionsTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
} else {
logger.break();
logger.log("No changes needed.");
}
logger.break();
logger.log(
`${highlighter.success("Success!")} React Grab options have been configured.`,
);
logger.break();
} catch (error) {
handleError(error);
}
});
================================================
FILE: packages/cli/src/commands/init.ts
================================================
import { existsSync } from "node:fs";
import { relative, resolve } from "node:path";
import { Command } from "commander";
import pc from "picocolors";
import { detectNonInteractive } from "../utils/is-non-interactive.js";
import { prompts } from "../utils/prompts.js";
import {
applyPackageJsonWithFeedback,
applyTransformWithFeedback,
formatInstalledAgentNames,
installPackagesWithFeedback,
uninstallPackagesWithFeedback,
} from "../utils/cli-helpers.js";
import {
promptConnectionMode,
promptMcpInstall,
} from "../utils/install-mcp.js";
import {
detectProject,
findReactProjects,
type Framework,
type PackageManager,
type UnsupportedFramework,
type WorkspaceProject,
} from "../utils/detect.js";
import { printDiff } from "../utils/diff.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import {
getPackagesToInstall,
getPackagesToUninstall,
} from "../utils/install.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";
import {
AGENTS,
type AgentIntegration,
getAgentDisplayName,
} from "../utils/templates.js";
import {
previewAgentRemoval,
previewOptionsTransform,
previewPackageJsonAgentRemoval,
previewPackageJsonTransform,
previewTransform,
type ReactGrabOptions,
} from "../utils/transform.js";
const VERSION = process.env.VERSION ?? "0.0.1";
const REPORT_URL = "https://react-grab.com/api/report-cli";
const DOCS_URL = "https://github.com/aidenybai/react-grab";
interface ReportConfig {
framework: string;
packageManager: string;
router?: string;
agent?: string;
isMonorepo: boolean;
}
const reportToCli = (
type: "error" | "completed",
config?: ReportConfig,
error?: Error,
): void => {
fetch(REPORT_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type,
version: VERSION,
config,
error: error ? { message: error.message, stack: error.stack } : undefined,
timestamp: new Date().toISOString(),
}),
}).catch(() => {});
};
const FRAMEWORK_NAMES: Record<Framework, string> = {
next: "Next.js",
vite: "Vite",
tanstack: "TanStack Start",
webpack: "Webpack",
unknown: "Unknown",
};
const PACKAGE_MANAGER_NAMES: Record<PackageManager, string> = {
npm: "npm",
yarn: "Yarn",
pnpm: "pnpm",
bun: "Bun",
};
const UNSUPPORTED_FRAMEWORK_NAMES: Record<
NonNullable<UnsupportedFramework>,
string
> = {
remix: "Remix",
astro: "Astro",
sveltekit: "SvelteKit",
gatsby: "Gatsby",
};
const getAgentName = getAgentDisplayName;
const sortProjectsByFramework = (
projects: WorkspaceProject[],
): WorkspaceProject[] =>
[...projects].sort((projectA, projectB) => {
if (projectA.framework === "unknown" && projectB.framework !== "unknown")
return 1;
if (projectA.framework !== "unknown" && projectB.framework === "unknown")
return -1;
return 0;
});
const printSubprojects = (
searchRoot: string,
sortedProjects: WorkspaceProject[],
): void => {
logger.break();
logger.log("Found the following projects:");
logger.break();
for (const project of sortedProjects) {
const frameworkLabel =
project.framework !== "unknown"
? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}`
: "";
const relativePath = relative(searchRoot, project.path);
logger.log(
` ${highlighter.info(project.name)}${frameworkLabel} ${highlighter.dim(relativePath)}`,
);
}
logger.break();
logger.log(
`Re-run with ${highlighter.info("-c <path>")} to specify a project:`,
);
logger.break();
logger.log(
` ${highlighter.dim("$")} npx -y grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`,
);
logger.break();
};
const formatActivationKeyDisplay = (
activationKey: ReactGrabOptions["activationKey"],
): string => {
if (!activationKey) return "Default (Option/Alt)";
return activationKey
.split("+")
.map((part) => {
const lower = part.toLowerCase();
if (lower === "meta") return process.platform === "darwin" ? "⌘" : "Win";
if (lower === "alt") return process.platform === "darwin" ? "⌥" : "Alt";
if (lower === "ctrl") return "Ctrl";
if (lower === "shift") return "Shift";
if (lower === "space" || lower === " ") return "Space";
return part.toUpperCase();
})
.join(" + ");
};
export const init = new Command()
.name("init")
.description("initialize React Grab in your project")
.option("-y, --yes", "skip confirmation prompts", false)
.option("-f, --force", "force overwrite existing config", false)
.option(
"-a, --agent <agent>",
`connect to your agent (${AGENTS.join(", ")}, mcp)`,
)
.option(
"-k, --key <key>",
"activation key (e.g., Meta+K, Ctrl+Shift+G, Space)",
)
.option("--skip-install", "skip package installation", false)
.option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)")
.option(
"-c, --cwd <cwd>",
"working directory (defaults to current directory)",
process.cwd(),
)
.action(async (opts) => {
console.log(
`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION)}`,
);
console.log();
try {
const cwd = resolve(opts.cwd);
const isNonInteractive = detectNonInteractive(opts.yes);
if (!existsSync(cwd)) {
logger.break();
logger.error(`Directory does not exist: ${highlighter.info(cwd)}`);
logger.break();
process.exit(1);
}
const preflightSpinner = spinner("Preflight checks.").start();
const projectInfo = await detectProject(cwd);
const removeAgents = async (
agentsToRemove: string[],
skipInstall: boolean = false,
) => {
for (const agentToRemove of agentsToRemove) {
const removalResult = previewAgentRemoval(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentToRemove,
);
const removalPackageJsonResult = previewPackageJsonAgentRemoval(
projectInfo.projectRoot,
agentToRemove,
);
if (!skipInstall) {
uninstallPackagesWithFeedback(
getPackagesToUninstall(agentToRemove),
projectInfo.packageManager,
projectInfo.projectRoot,
);
}
if (
removalResult.success &&
!removalResult.noChanges &&
removalResult.newContent
) {
applyTransformWithFeedback(
removalResult,
`Removing ${getAgentName(agentToRemove)} from ${removalResult.filePath}.`,
);
}
if (
removalPackageJsonResult.success &&
!removalPackageJsonResult.noChanges &&
removalPackageJsonResult.newContent
) {
applyPackageJsonWithFeedback(
removalPackageJsonResult,
`Removing ${getAgentName(agentToRemove)} from ${removalPackageJsonResult.filePath}.`,
);
}
}
};
if (projectInfo.hasReactGrab && !opts.force) {
preflightSpinner.succeed();
if (isNonInteractive) {
logger.break();
logger.warn("React Grab is already installed.");
logger.log(
`Use ${highlighter.info("--force")} to reconfigure, or remove ${highlighter.info("--yes")} for interactive mode.`,
);
logger.break();
process.exit(0);
}
logger.break();
logger.success("React Grab is already installed.");
logger.break();
if (projectInfo.installedAgents.length > 0) {
logger.log(
`Currently installed agents: ${highlighter.info(formatInstalledAgentNames(projectInfo.installedAgents))}`,
);
logger.break();
}
const { wantCustomizeOptions } = await prompts({
type: "confirm",
name: "wantCustomizeOptions",
message: `Would you like to customize ${highlighter.info("options")}?`,
initial: false,
});
if (wantCustomizeOptions === undefined) {
logger.break();
process.exit(1);
}
if (wantCustomizeOptions || opts.key) {
logger.break();
logger.log(`Configure ${highlighter.info("React Grab")} options:`);
logger.break();
const collectedOptions: ReactGrabOptions = {};
if (opts.key) {
collectedOptions.activationKey = opts.key;
logger.log(
` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,
);
} else {
const { wantActivationKey } = await prompts({
type: "confirm",
name: "wantActivationKey",
message: `Configure ${highlighter.info("activation key")}?`,
initial: false,
});
if (wantActivationKey === undefined) {
logger.break();
process.exit(1);
}
if (wantActivationKey) {
const { key } = await prompts({
type: "text",
name: "key",
message: "Enter the activation key (e.g., g, k, space):",
initial: "",
});
if (key === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.activationKey = key
? key.toLowerCase()
: undefined;
logger.log(
` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`,
);
}
}
const { activationMode } = await prompts({
type: "select",
name: "activationMode",
message: `Select ${highlighter.info("activation mode")}:`,
choices: [
{
title: "Toggle (press to activate/deactivate)",
value: "toggle",
},
{ title: "Hold (hold key to keep active)", value: "hold" },
],
initial: 0,
});
if (activationMode === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.activationMode = activationMode;
if (activationMode === "hold") {
const { keyHoldDuration } = await prompts({
type: "number",
name: "keyHoldDuration",
message: `Enter ${highlighter.info("key hold duration")} in milliseconds:`,
initial: 150,
min: 0,
max: 2000,
});
if (keyHoldDuration === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.keyHoldDuration = keyHoldDuration;
}
const { allowActivationInsideInput } = await prompts({
type: "confirm",
name: "allowActivationInsideInput",
message: `Allow activation ${highlighter.info("inside input fields")}?`,
initial: true,
});
if (allowActivationInsideInput === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.allowActivationInsideInput =
allowActivationInsideInput;
const { maxContextLines } = await prompts({
type: "number",
name: "maxContextLines",
message: `Enter ${highlighter.info("max context lines")} to include:`,
initial: 3,
min: 0,
max: 50,
});
if (maxContextLines === undefined) {
logger.break();
process.exit(1);
}
collectedOptions.maxContextLines = maxContextLines;
const optionsResult = previewOptionsTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
collectedOptions,
);
if (!optionsResult.success) {
logger.break();
logger.error(optionsResult.message);
logger.break();
process.exit(1);
}
const hasOptionsChanges =
!optionsResult.noChanges &&
optionsResult.originalContent &&
optionsResult.newContent;
if (hasOptionsChanges) {
logger.break();
printDiff(
optionsResult.filePath,
optionsResult.originalContent!,
optionsResult.newContent!,
);
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Options configuration cancelled.");
} else {
applyTransformWithFeedback(optionsResult);
logger.break();
logger.success("React Grab options have been configured.");
}
} else {
logger.break();
logger.log("No option changes needed.");
}
}
const availableAgents = AGENTS.filter(
(agent) => !projectInfo.installedAgents.includes(agent),
);
logger.break();
const { wantAddAgent } = await prompts({
type: "confirm",
name: "wantAddAgent",
message: `Would you like to ${highlighter.info("connect it to your agent")}?`,
initial: false,
});
if (wantAddAgent === undefined) {
logger.break();
process.exit(1);
}
if (wantAddAgent) {
const connectionMode = await promptConnectionMode();
if (connectionMode === undefined) {
logger.break();
process.exit(1);
}
let agentIntegration: AgentIntegration;
if (connectionMode === "mcp") {
const didInstall = await promptMcpInstall();
if (!didInstall) {
logger.break();
process.exit(0);
}
logger.break();
logger.success("MCP server has been configured.");
logger.log("Restart your agents to activate.");
} else {
const { agent } = await prompts({
type: "select",
name: "agent",
message: `Which ${highlighter.info("agent")} would you like to connect?`,
choices: [
...availableAgents.map((innerAgent) => ({
title: getAgentName(innerAgent),
value: innerAgent,
})),
{ title: "Skip", value: "skip" },
],
});
if (agent === undefined || agent === "skip") {
logger.break();
process.exit(0);
}
agentIntegration = agent as AgentIntegration;
let agentsToRemove: string[] = [];
if (projectInfo.installedAgents.length > 0) {
const installedNames = formatInstalledAgentNames(
projectInfo.installedAgents,
);
const { action } = await prompts({
type: "select",
name: "action",
message: "How would you like to proceed?",
choices: [
{
title: `Replace ${installedNames} with ${getAgentName(agentIntegration)}`,
value: "replace",
},
{
title: `Add ${getAgentName(agentIntegration)} alongside existing`,
value: "add",
},
{ title: "Cancel", value: "cancel" },
],
});
if (!action || action === "cancel") {
logger.break();
logger.log("Agent addition cancelled.");
} else {
if (action === "replace") {
agentsToRemove = [...projectInfo.installedAgents];
}
if (agentsToRemove.length > 0) {
await removeAgents(agentsToRemove);
projectInfo.installedAgents =
projectInfo.installedAgents.filter(
(innerAgent) => !agentsToRemove.includes(innerAgent),
);
}
const result = previewTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentIntegration,
true,
);
const packageJsonResult = previewPackageJsonTransform(
projectInfo.projectRoot,
agentIntegration,
projectInfo.installedAgents,
projectInfo.packageManager,
);
if (!result.success) {
logger.break();
logger.error(result.message);
logger.break();
process.exit(1);
}
const hasLayoutChanges =
!result.noChanges &&
result.originalContent &&
result.newContent;
const hasPackageJsonChanges =
packageJsonResult.success &&
!packageJsonResult.noChanges &&
packageJsonResult.originalContent &&
packageJsonResult.newContent;
if (hasLayoutChanges || hasPackageJsonChanges) {
logger.break();
if (hasLayoutChanges) {
printDiff(
result.filePath,
result.originalContent!,
result.newContent!,
);
}
if (hasPackageJsonChanges) {
if (hasLayoutChanges) {
logger.break();
}
printDiff(
packageJsonResult.filePath,
packageJsonResult.originalContent!,
packageJsonResult.newContent!,
);
}
if (agentsToRemove.length === 0) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Agent addition cancelled.");
} else {
installPackagesWithFeedback(
getPackagesToInstall(agentIntegration, false),
projectInfo.packageManager,
projectInfo.projectRoot,
);
if (hasLayoutChanges) {
applyTransformWithFeedback(result);
}
if (hasPackageJsonChanges) {
applyPackageJsonWithFeedback(packageJsonResult);
}
logger.break();
logger.success(
`${getAgentName(agentIntegration)} has been added.`,
);
}
} else {
installPackagesWithFeedback(
getPackagesToInstall(agentIntegration, false),
projectInfo.packageManager,
projectInfo.projectRoot,
);
if (hasLayoutChanges) {
applyTransformWithFeedback(result);
}
if (hasPackageJsonChanges) {
applyPackageJsonWithFeedback(packageJsonResult);
}
logger.break();
logger.success(
`${getAgentName(agentIntegration)} has been added.`,
);
}
}
}
} else {
const result = previewTransform(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentIntegration,
true,
);
const packageJsonResult = previewPackageJsonTransform(
projectInfo.projectRoot,
agentIntegration,
projectInfo.installedAgents,
projectInfo.packageManager,
);
if (!result.success) {
logger.break();
logger.error(result.message);
logger.break();
process.exit(1);
}
const hasLayoutChanges =
!result.noChanges &&
result.originalContent &&
result.newContent;
const hasPackageJsonChanges =
packageJsonResult.success &&
!packageJsonResult.noChanges &&
packageJsonResult.originalContent &&
packageJsonResult.newContent;
if (hasLayoutChanges || hasPackageJsonChanges) {
logger.break();
if (hasLayoutChanges) {
printDiff(
result.filePath,
result.originalContent!,
result.newContent!,
);
}
if (hasPackageJsonChanges) {
if (hasLayoutChanges) {
logger.break();
}
printDiff(
packageJsonResult.filePath,
packageJsonResult.originalContent!,
packageJsonResult.newContent!,
);
}
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Agent addition cancelled.");
} else {
installPackagesWithFeedback(
getPackagesToInstall(agentIntegration, false),
projectInfo.packageManager,
projectInfo.projectRoot,
);
if (hasLayoutChanges) {
applyTransformWithFeedback(result);
}
if (hasPackageJsonChanges) {
applyPackageJsonWithFeedback(packageJsonResult);
}
logger.break();
logger.success(
`${getAgentName(agentIntegration)} has been added.`,
);
}
}
}
}
}
logger.break();
process.exit(0);
}
preflightSpinner.succeed();
const frameworkSpinner = spinner("Verifying framework.").start();
if (projectInfo.unsupportedFramework) {
const frameworkName =
UNSUPPORTED_FRAMEWORK_NAMES[projectInfo.unsupportedFramework];
frameworkSpinner.fail(`Found ${highlighter.info(frameworkName)}.`);
logger.break();
logger.log(`${frameworkName} is not yet supported by automatic setup.`);
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
logger.break();
process.exit(1);
}
if (projectInfo.framework === "unknown") {
let searchRoot = cwd;
let reactProjects = findReactProjects(searchRoot);
if (reactProjects.length === 0 && cwd !== process.cwd()) {
searchRoot = process.cwd();
reactProjects = findReactProjects(searchRoot);
}
if (reactProjects.length > 0) {
frameworkSpinner.info(
`Verifying framework. Found ${reactProjects.length} project${reactProjects.length === 1 ? "" : "s"}.`,
);
const sortedProjects = sortProjectsByFramework(reactProjects);
if (isNonInteractive) {
printSubprojects(searchRoot, sortedProjects);
process.exit(1);
}
logger.break();
const { selectedProject } = await prompts({
type: "select",
name: "selectedProject",
message: "Select a project to install React Grab:",
choices: [
...sortedProjects.map((project) => {
const frameworkLabel =
project.framework !== "unknown"
? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}`
: "";
return {
title: `${project.name}${frameworkLabel}`,
value: project.path,
};
}),
{ title: "Skip", value: "skip" },
],
});
if (!selectedProject || selectedProject === "skip") {
logger.break();
process.exit(0);
}
process.chdir(selectedProject);
const newProjectInfo = await detectProject(selectedProject);
Object.assign(projectInfo, newProjectInfo);
const newFrameworkSpinner = spinner("Verifying framework.").start();
newFrameworkSpinner.succeed(
`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`,
);
} else {
frameworkSpinner.fail("Could not detect a supported framework.");
logger.break();
logger.log(
"React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.",
);
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
logger.break();
process.exit(1);
}
} else {
frameworkSpinner.succeed(
`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`,
);
}
if (projectInfo.framework === "next") {
const routerSpinner = spinner("Detecting router type.").start();
routerSpinner.succeed(
`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`,
);
}
const packageManagerSpinner = spinner(
"Detecting package manager.",
).start();
packageManagerSpinner.succeed(
`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`,
);
const finalFramework = projectInfo.framework;
const finalPackageManager = projectInfo.packageManager;
const finalNextRouterType = projectInfo.nextRouterType;
let agentIntegration: AgentIntegration =
(opts.agent as AgentIntegration) || "none";
const agentsToRemove: string[] = [];
if (!isNonInteractive && !opts.agent) {
logger.break();
const { wantAddAgent } = await prompts({
type: "confirm",
name: "wantAddAgent",
message: `Would you like to ${highlighter.info("connect it to your agent")}?`,
initial: false,
});
if (wantAddAgent === undefined) {
logger.break();
process.exit(1);
}
if (wantAddAgent) {
const connectionMode = await promptConnectionMode();
if (connectionMode === undefined) {
logger.break();
process.exit(1);
}
if (connectionMode === "mcp") {
const didInstall = await promptMcpInstall();
if (!didInstall) {
logger.break();
process.exit(0);
}
logger.break();
logger.success("MCP server has been configured.");
logger.log("Continuing with React Grab installation...");
logger.break();
agentIntegration = "mcp";
} else {
const { agent } = await prompts({
type: "select",
name: "agent",
message: `Which ${highlighter.info("agent")} would you like to connect?`,
choices: [
...AGENTS.map((innerAgent) => ({
title: getAgentName(innerAgent),
value: innerAgent,
})),
{ title: "Skip", value: "skip" },
],
});
if (agent === undefined) {
logger.break();
process.exit(1);
}
if (agent !== "skip") {
agentIntegration = agent as AgentIntegration;
}
}
}
}
const result = previewTransform(
projectInfo.projectRoot,
finalFramework,
finalNextRouterType,
agentIntegration,
false,
opts.force,
);
const packageJsonResult = previewPackageJsonTransform(
projectInfo.projectRoot,
agentIntegration,
projectInfo.installedAgents,
finalPackageManager,
);
if (!result.success) {
logger.break();
logger.error(result.message);
logger.error(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
logger.break();
process.exit(1);
}
const hasLayoutChanges =
!result.noChanges && result.originalContent && result.newContent;
const hasPackageJsonChanges =
packageJsonResult.success &&
!packageJsonResult.noChanges &&
packageJsonResult.originalContent &&
packageJsonResult.newContent;
if (hasLayoutChanges || hasPackageJsonChanges) {
logger.break();
if (hasLayoutChanges) {
printDiff(
result.filePath,
result.originalContent!,
result.newContent!,
);
}
if (hasPackageJsonChanges) {
if (hasLayoutChanges) {
logger.break();
}
printDiff(
packageJsonResult.filePath,
packageJsonResult.originalContent!,
packageJsonResult.newContent!,
);
}
logger.break();
logger.warn("Auto-detection may not be 100% accurate.");
logger.warn("Please verify the changes before committing.");
if (!isNonInteractive) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
}
}
if (agentsToRemove.length > 0) {
await removeAgents(agentsToRemove, opts.skipInstall);
projectInfo.installedAgents = projectInfo.installedAgents.filter(
(agent) => !agentsToRemove.includes(agent),
);
}
const shouldInstallReactGrab = !projectInfo.hasReactGrab;
const shouldInstallAgent =
agentIntegration !== "none" &&
!projectInfo.installedAgents.includes(agentIntegration);
if (!opts.skipInstall && (shouldInstallReactGrab || shouldInstallAgent)) {
installPackagesWithFeedback(
getPackagesToInstall(agentIntegration, shouldInstallReactGrab),
finalPackageManager,
projectInfo.projectRoot,
);
}
if (hasLayoutChanges) {
applyTransformWithFeedback(result);
}
if (hasPackageJsonChanges) {
applyPackageJsonWithFeedback(packageJsonResult);
}
logger.break();
logger.log(
`${highlighter.success("Success!")} React Grab has been installed.`,
);
if (packageJsonResult.warning) {
logger.break();
logger.warn(packageJsonResult.warning);
logger.break();
} else {
logger.log("You may now start your development server.");
}
logger.break();
reportToCli("completed", {
framework: finalFramework,
packageManager: finalPackageManager,
router: finalNextRouterType,
agent: agentIntegration !== "none" ? agentIntegration : undefined,
isMonorepo: projectInfo.isMonorepo,
});
} catch (error) {
handleError(error);
reportToCli("error", undefined, error as Error);
}
});
================================================
FILE: packages/cli/src/commands/remove.ts
================================================
import { Command } from "commander";
import pc from "picocolors";
import { detectNonInteractive } from "../utils/is-non-interactive.js";
import { prompts } from "../utils/prompts.js";
import { detectProject } from "../utils/detect.js";
import { printDiff } from "../utils/diff.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import { getPackagesToUninstall, uninstallPackages } from "../utils/install.js";
import { logger } from "../utils/logger.js";
import { spinner } from "../utils/spinner.js";
import { AGENTS, getAgentDisplayName } from "../utils/templates.js";
import {
applyPackageJsonTransform,
applyTransform,
previewAgentRemoval,
previewPackageJsonAgentRemoval,
} from "../utils/transform.js";
const VERSION = process.env.VERSION ?? "0.0.1";
export const remove = new Command()
.name("remove")
.description("disconnect React Grab from your agent")
.argument("[agent]", `agent to disconnect (${AGENTS.join(", ")}, mcp)`)
.option("-y, --yes", "skip confirmation prompts", false)
.option(
"-c, --cwd <cwd>",
"working directory (defaults to current directory)",
process.cwd(),
)
.action(async (agentArg, opts) => {
console.log(
`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION)}`,
);
console.log();
try {
const cwd = opts.cwd;
const isNonInteractive = detectNonInteractive(opts.yes);
const preflightSpinner = spinner("Preflight checks.").start();
const projectInfo = await detectProject(cwd);
if (!projectInfo.hasReactGrab) {
preflightSpinner.fail("React Grab is not installed.");
logger.break();
logger.error(
`Run ${highlighter.info("react-grab init")} first to install React Grab.`,
);
logger.break();
process.exit(1);
}
if (projectInfo.installedAgents.length === 0) {
preflightSpinner.succeed();
logger.break();
logger.warn("No agent connections are installed.");
logger.break();
process.exit(0);
}
preflightSpinner.succeed();
let agentToRemove: string;
if (agentArg) {
if (!projectInfo.installedAgents.includes(agentArg)) {
logger.break();
logger.error(`Agent ${highlighter.info(agentArg)} is not installed.`);
logger.log(
`Installed agents: ${projectInfo.installedAgents.map(getAgentDisplayName).join(", ")}`,
);
logger.break();
process.exit(1);
}
agentToRemove = agentArg;
} else if (!isNonInteractive) {
logger.break();
const { agent } = await prompts({
type: "select",
name: "agent",
message: `Which ${highlighter.info("agent")} would you like to disconnect?`,
choices: projectInfo.installedAgents.map((innerAgent) => ({
title: getAgentDisplayName(innerAgent),
value: innerAgent,
})),
});
if (!agent) {
logger.break();
process.exit(1);
}
agentToRemove = agent;
} else {
logger.break();
logger.error("Please specify an agent to disconnect.");
logger.error(
"Installed agents: " + projectInfo.installedAgents.join(", "),
);
logger.break();
process.exit(1);
}
const removingSpinner = spinner(
`Preparing to remove ${getAgentDisplayName(agentToRemove)}.`,
).start();
removingSpinner.succeed();
const result = previewAgentRemoval(
projectInfo.projectRoot,
projectInfo.framework,
projectInfo.nextRouterType,
agentToRemove,
);
const packageJsonResult = previewPackageJsonAgentRemoval(
projectInfo.projectRoot,
agentToRemove,
);
const hasLayoutChanges =
result.success &&
!result.noChanges &&
result.originalContent &&
result.newContent;
const hasPackageJsonChanges =
packageJsonResult.success &&
!packageJsonResult.noChanges &&
packageJsonResult.originalContent &&
packageJsonResult.newContent;
if (hasLayoutChanges || hasPackageJsonChanges) {
logger.break();
if (hasLayoutChanges) {
printDiff(
result.filePath,
result.originalContent!,
result.newContent!,
);
}
if (hasPackageJsonChanges) {
if (hasLayoutChanges) {
logger.break();
}
printDiff(
packageJsonResult.filePath,
packageJsonResult.originalContent!,
packageJsonResult.newContent!,
);
}
if (!isNonInteractive) {
logger.break();
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Apply these changes?",
initial: true,
});
if (!proceed) {
logger.break();
logger.log("Changes cancelled.");
logger.break();
process.exit(0);
}
}
}
const packages = getPackagesToUninstall(agentToRemove);
if (packages.length > 0) {
const uninstallSpinner = spinner(
`Removing ${packages.join(", ")}.`,
).start();
try {
uninstallPackages(
packages,
projectInfo.packageManager,
projectInfo.projectRoot,
);
uninstallSpinner.succeed();
} catch (error) {
uninstallSpinner.fail();
handleError(error);
}
}
if (hasLayoutChanges) {
const writeSpinner = spinner(
`Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
}
if (hasPackageJsonChanges) {
const packageJsonSpinner = spinner(
`Applying changes to ${packageJsonResult.filePath}.`,
).start();
const packageJsonWriteResult =
applyPackageJsonTransform(packageJsonResult);
if (!packageJsonWriteResult.success) {
packageJsonSpinner.fail();
logger.break();
logger.error(packageJsonWriteResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
packageJsonSpinner.succeed();
}
logger.break();
logger.log(
`${highlighter.success("Success!")} ${getAgentDisplayName(agentToRemove)} has been removed.`,
);
logger.break();
} catch (error) {
handleError(error);
}
});
================================================
FILE: packages/cli/src/utils/cli-helpers.ts
================================================
import type { PackageManager } from "./detect.js";
import type {
PackageJsonTransformResult,
TransformResult,
} from "./transform.js";
import { applyPackageJsonTransform, applyTransform } from "./transform.js";
import { handleError } from "./handle-error.js";
import { installPackages, uninstallPackages } from "./install.js";
import { logger } from "./logger.js";
import { spinner } from "./spinner.js";
import { getAgentDisplayName } from "./templates.js";
export const formatInstalledAgentNames = (agents: string[]): string =>
agents.map(getAgentDisplayName).join(", ");
export const applyTransformWithFeedback = (
result: TransformResult,
message?: string,
): void => {
const writeSpinner = spinner(
message ?? `Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
};
export const applyPackageJsonWithFeedback = (
result: PackageJsonTransformResult,
message?: string,
): void => {
const writeSpinner = spinner(
message ?? `Applying changes to ${result.filePath}.`,
).start();
const writeResult = applyPackageJsonTransform(result);
if (!writeResult.success) {
writeSpinner.fail();
logger.break();
logger.error(writeResult.error || "Failed to write file.");
logger.break();
process.exit(1);
}
writeSpinner.succeed();
};
export const installPackagesWithFeedback = (
packages: string[],
packageManager: PackageManager,
projectRoot: string,
): void => {
if (packages.length === 0) return;
const installSpinner = spinner(`Installing ${packages.join(", ")}.`).start();
try {
installPackages(packages, packageManager, projectRoot);
installSpinner.succeed();
} catch (error) {
installSpinner.fail();
handleError(error);
}
};
export const uninstallPackagesWithFeedback = (
packages: string[],
packageManager: PackageManager,
projectRoot: string,
): void => {
if (packages.length === 0) return;
const uninstallSpinner = spinner(`Removing ${packages.join(", ")}.`).start();
try {
uninstallPackages(packages, packageManager, projectRoot);
uninstallSpinner.succeed();
} catch (error) {
uninstallSpinner.fail();
handleError(error);
}
};
================================================
FILE: packages/cli/src/utils/constants.ts
================================================
export const MAX_SUGGESTIONS_COUNT = 30;
export const MAX_KEY_HOLD_DURATION_MS = 2000;
export const MAX_CONTEXT_LINES = 50;
================================================
FILE: packages/cli/src/utils/detect.ts
================================================
import { execSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { detect } from "@antfu/ni";
import ignore from "ignore";
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
export type Framework = "next" | "vite" | "tanstack" | "webpack" | "unknown";
export type NextRouterType = "app" | "pages" | "unknown";
export type UnsupportedFramework =
| "remix"
| "astro"
| "sveltekit"
| "gatsby"
| null;
export interface ProjectInfo {
packageManager: PackageManager;
framework: Framework;
nextRouterType: NextRouterType;
isMonorepo: boolean;
projectRoot: string;
hasReactGrab: boolean;
installedAgents: string[];
unsupportedFramework: UnsupportedFramework;
}
const VALID_PACKAGE_MANAGERS: ReadonlySet<string> = new Set([
"npm",
"yarn",
"pnpm",
"bun",
]);
export const detectPackageManager = async (
projectRoot: string,
): Promise<PackageManager> => {
const detected = await detect({ cwd: projectRoot });
if (detected) {
// @antfu/ni returns versioned agents like "pnpm@6" or "yarn@berry"
const managerName = detected.split("@")[0];
if (VALID_PACKAGE_MANAGERS.has(managerName)) {
return managerName as PackageManager;
}
}
return "npm";
};
export const detectFramework = (projectRoot: string): Framework => {
const packageJsonPath = join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return "unknown";
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (allDependencies["next"]) {
return "next";
}
if (allDependencies["@tanstack/react-start"]) {
return "tanstack";
}
if (allDependencies["vite"]) {
return "vite";
}
if (allDependencies["webpack"]) {
return "webpack";
}
return "unknown";
} catch {
return "unknown";
}
};
export const detectNextRouterType = (projectRoot: string): NextRouterType => {
const hasAppDir = existsSync(join(projectRoot, "app"));
const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
const hasPagesDir = existsSync(join(projectRoot, "pages"));
const hasSrcPagesDir = existsSync(join(projectRoot, "src", "pages"));
if (hasAppDir || hasSrcAppDir) {
return "app";
}
if (hasPagesDir || hasSrcPagesDir) {
return "pages";
}
return "unknown";
};
export const detectMonorepo = (projectRoot: string): boolean => {
if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) {
return true;
}
if (existsSync(join(projectRoot, "lerna.json"))) {
return true;
}
const packageJsonPath = join(projectRoot, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
if (packageJson.workspaces) {
return true;
}
} catch {
return false;
}
}
return false;
};
export interface WorkspaceProject {
name: string;
path: string;
framework: Framework;
hasReact: boolean;
}
const getWorkspacePatterns = (projectRoot: string): string[] => {
const patterns: string[] = [];
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
if (existsSync(pnpmWorkspacePath)) {
const content = readFileSync(pnpmWorkspacePath, "utf-8");
const lines = content.split("\n");
let inPackages = false;
for (const line of lines) {
if (line.match(/^packages:\s*$/)) {
inPackages = true;
continue;
}
if (inPackages) {
if (line.match(/^[a-zA-Z]/) || line.trim() === "") {
if (line.match(/^[a-zA-Z]/)) inPackages = false;
continue;
}
const match = line.match(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/);
if (match) {
patterns.push(match[1].trim());
}
}
}
}
const lernaJsonPath = join(projectRoot, "lerna.json");
if (existsSync(lernaJsonPath)) {
try {
const lernaJson = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
if (Array.isArray(lernaJson.packages)) {
patterns.push(...lernaJson.packages);
}
} catch {}
}
const packageJsonPath = join(projectRoot, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
if (Array.isArray(packageJson.workspaces)) {
patterns.push(...packageJson.workspaces);
} else if (packageJson.workspaces?.packages) {
patterns.push(...packageJson.workspaces.packages);
}
} catch {}
}
return [...new Set(patterns)];
};
const expandWorkspacePattern = (
projectRoot: string,
pattern: string,
): string[] => {
const isGlob = pattern.endsWith("/*");
const cleanPattern = pattern.replace(/\/\*$/, "");
const basePath = join(projectRoot, cleanPattern);
if (!existsSync(basePath)) return [];
if (!isGlob) {
const hasPackageJson = existsSync(join(basePath, "package.json"));
return hasPackageJson ? [basePath] : [];
}
const results: string[] = [];
try {
const entries = readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const packageJsonPath = join(basePath, entry.name, "package.json");
if (existsSync(packageJsonPath)) {
results.push(join(basePath, entry.name));
}
}
} catch {
return results;
}
return results;
};
const hasReactDependency = (projectPath: string): boolean => {
const packageJsonPath = join(projectPath, "package.json");
if (!existsSync(packageJsonPath)) return false;
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
return Boolean(allDeps["react"] || allDeps["react-dom"]);
} catch {
return false;
}
};
const buildReactProject = (projectPath: string): WorkspaceProject | null => {
const framework = detectFramework(projectPath);
const hasReact = hasReactDependency(projectPath);
if (!hasReact && framework === "unknown") return null;
let name = basename(projectPath);
const packageJsonPath = join(projectPath, "package.json");
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
name = packageJson.name || name;
} catch {}
return { name, path: projectPath, framework, hasReact };
};
const findWorkspaceProjects = (projectRoot: string): WorkspaceProject[] => {
const patterns = getWorkspacePatterns(projectRoot);
const projects: WorkspaceProject[] = [];
for (const pattern of patterns) {
for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {
const project = buildReactProject(projectPath);
if (project) projects.push(project);
}
}
return projects;
};
const ALWAYS_IGNORED_DIRECTORIES = [
"node_modules",
".git",
".next",
".cache",
".turbo",
"dist",
"build",
"coverage",
"test-results",
];
const loadGitignore = (projectRoot: string): ReturnType<typeof ignore> => {
const ignorer = ignore().add(ALWAYS_IGNORED_DIRECTORIES);
const gitignorePath = join(projectRoot, ".gitignore");
if (existsSync(gitignorePath)) {
try {
ignorer.add(readFileSync(gitignorePath, "utf-8"));
} catch {}
}
return ignorer;
};
const scanDirectoryForProjects = (
rootDirectory: string,
ignorer: ReturnType<typeof ignore>,
maxDepth: number,
currentDepth: number = 0,
): WorkspaceProject[] => {
if (currentDepth >= maxDepth) return [];
if (!existsSync(rootDirectory)) return [];
const projects: WorkspaceProject[] = [];
try {
const entries = readdirSync(rootDirectory, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (ignorer.ignores(entry.name)) continue;
const entryPath = join(rootDirectory, entry.name);
const hasPackageJson = existsSync(join(entryPath, "package.json"));
if (hasPackageJson) {
const project = buildReactProject(entryPath);
if (project) {
projects.push(project);
continue;
}
}
projects.push(
...scanDirectoryForProjects(
entryPath,
ignorer,
maxDepth,
currentDepth + 1,
),
);
}
} catch {
return projects;
}
return projects;
};
const MAX_SCAN_DEPTH = 2;
export const findReactProjects = (projectRoot: string): WorkspaceProject[] => {
if (detectMonorepo(projectRoot)) {
const workspaceProjects = findWorkspaceProjects(projectRoot);
if (workspaceProjects.length > 0) {
return workspaceProjects;
}
}
const ignorer = loadGitignore(projectRoot);
const scannedProjects = scanDirectoryForProjects(
projectRoot,
ignorer,
MAX_SCAN_DEPTH,
);
if (scannedProjects.length > 0) {
return scannedProjects;
}
let currentDirectory = dirname(projectRoot);
while (currentDirectory !== dirname(currentDirectory)) {
const parentProject = buildReactProject(currentDirectory);
if (parentProject) {
return [parentProject];
}
currentDirectory = dirname(currentDirectory);
}
return [];
};
const hasReactGrabInFile = (filePath: string): boolean => {
if (!existsSync(filePath)) return false;
try {
const content = readFileSync(filePath, "utf-8");
const fuzzyPatterns = [
/["'`][^"'`]*react-grab/,
/react-grab[^"'`]*["'`]/,
/<[^>]*react-grab/i,
/import[^;]*react-grab/i,
/require[^)]*react-grab/i,
/from\s+[^;]*react-grab/i,
/src[^>]*react-grab/i,
];
return fuzzyPatterns.some((pattern) => pattern.test(content));
} catch {
return false;
}
};
export const detectReactGrab = (projectRoot: string): boolean => {
const packageJsonPath = join(projectRoot, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (allDependencies["react-grab"]) {
return true;
}
} catch {}
}
const filesToCheck = [
join(projectRoot, "app", "layout.tsx"),
join(projectRoot, "app", "layout.jsx"),
join(projectRoot, "src", "app", "layout.tsx"),
join(projectRoot, "src", "app", "layout.jsx"),
join(projectRoot, "pages", "_document.tsx"),
join(projectRoot, "pages", "_document.jsx"),
join(projectRoot, "instrumentation-client.ts"),
join(projectRoot, "instrumentation-client.js"),
join(projectRoot, "src", "instrumentation-client.ts"),
join(projectRoot, "src", "instrumentation-client.js"),
join(projectRoot, "index.html"),
join(projectRoot, "public", "index.html"),
join(projectRoot, "src", "index.tsx"),
join(projectRoot, "src", "index.ts"),
join(projectRoot, "src", "main.tsx"),
join(projectRoot, "src", "main.ts"),
join(projectRoot, "src", "routes", "__root.tsx"),
join(projectRoot, "src", "routes", "__root.jsx"),
join(projectRoot, "app", "routes", "__root.tsx"),
join(projectRoot, "app", "routes", "__root.jsx"),
];
return filesToCheck.some(hasReactGrabInFile);
};
const AGENT_PACKAGES = [
"@react-grab/claude-code",
"@react-grab/cursor",
"@react-grab/opencode",
"@react-grab/codex",
"@react-grab/gemini",
"@react-grab/amp",
"@react-grab/droid",
"@react-grab/copilot",
"@react-grab/mcp",
];
export const detectUnsupportedFramework = (
projectRoot: string,
): UnsupportedFramework => {
const packageJsonPath = join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return null;
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
if (allDependencies["@remix-run/react"] || allDependencies["remix"]) {
return "remix";
}
if (allDependencies["astro"]) {
return "astro";
}
if (allDependencies["@sveltejs/kit"]) {
return "sveltekit";
}
if (allDependencies["gatsby"]) {
return "gatsby";
}
return null;
} catch {
return null;
}
};
export const detectInstalledAgents = (projectRoot: string): string[] => {
const packageJsonPath = join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return [];
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const allDependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
return AGENT_PACKAGES.filter((agent) =>
Boolean(allDependencies[agent]),
).map((agent) => agent.replace("@react-grab/", ""));
} catch {
return [];
}
};
export type AgentCLI =
| "claude"
| "cursor-agent"
| "opencode"
| "codex"
| "gemini"
| "amp"
| "copilot"
| "droid";
const AGENT_CLI_COMMANDS: AgentCLI[] = [
"claude",
"cursor-agent",
"opencode",
"codex",
"gemini",
"amp",
"copilot",
"droid",
];
const isCommandAvailable = (command: string): boolean => {
try {
execSync(`which ${command}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
};
export const detectAvailableAgentCLIs = (): AgentCLI[] => {
return AGENT_CLI_COMMANDS.filter(isCommandAvailable);
};
export const detectProject = async (
projectRoot: string = process.cwd(),
): Promise<ProjectInfo> => {
const framework = detectFramework(projectRoot);
const packageManager = await detectPackageManager(projectRoot);
return {
packageManager,
framework,
nextRouterType:
framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
isMonorepo: detectMonorepo(projectRoot),
projectRoot,
hasReactGrab: detectReactGrab(projectRoot),
installedAgents: detectInstalledAgents(projectRoot),
unsupportedFramework: detectUnsupportedFramework(projectRoot),
};
};
================================================
FILE: packages/cli/src/utils/diff.ts
================================================
interface DiffLine {
type: "added" | "removed" | "unchanged";
content: string;
lineNumber?: number;
}
const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
const GRAY = "\x1b[90m";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
export const generateDiff = (
originalContent: string,
newContent: string,
): DiffLine[] => {
const originalLines = originalContent.split("\n");
const newLines = newContent.split("\n");
const diff: DiffLine[] = [];
const maxLength = Math.max(originalLines.length, newLines.length);
let originalIndex = 0;
let newIndex = 0;
while (originalIndex < originalLines.length || newIndex < newLines.length) {
const originalLine = originalLines[originalIndex];
const newLine = newLines[newIndex];
if (originalLine === newLine) {
diff.push({
type: "unchanged",
content: originalLine,
lineNumber: newIndex + 1,
});
originalIndex++;
newIndex++;
} else if (originalLine === undefined) {
diff.push({ type: "added", content: newLine, lineNumber: newIndex + 1 });
newIndex++;
} else if (newLine === undefined) {
diff.push({ type: "removed", content: originalLine });
originalIndex++;
} else {
const originalInNew = newLines.indexOf(originalLine, newIndex);
const newInOriginal = originalLines.indexOf(newLine, originalIndex);
if (
originalInNew !== -1 &&
(newInOriginal === -1 ||
originalInNew - newIndex < newInOriginal - originalIndex)
) {
while (newIndex < originalInNew) {
diff.push({
type: "added",
content: newLines[newIndex],
lineNumber: newIndex + 1,
});
newIndex++;
}
} else if (newInOriginal !== -1) {
while (originalIndex < newInOriginal) {
diff.push({ type: "removed", content: originalLines[originalIndex] });
originalIndex++;
}
} else {
diff.push({ type: "removed", content: originalLine });
diff.push({
type: "added",
content: newLine,
lineNumber: newIndex + 1,
});
originalIndex++;
newIndex++;
}
}
}
return diff;
};
export const formatDiff = (
diff: DiffLine[],
contextLines: number = 3,
): string => {
const lines: string[] = [];
let lastPrintedIndex = -1;
let hasChanges = false;
const changedIndices = diff
.map((line, index) => (line.type !== "unchanged" ? index : -1))
.filter((index) => index !== -1);
if (changedIndices.length === 0) {
return `${GRAY}No changes${RESET}`;
}
for (const changedIndex of changedIndices) {
const startContext = Math.max(0, changedIndex - contextLines);
const endContext = Math.min(diff.length - 1, changedIndex + contextLines);
if (startContext > lastPrintedIndex + 1 && lastPrintedIndex !== -1) {
lines.push(`${GRAY} ...${RESET}`);
}
for (
let lineIndex = Math.max(startContext, lastPrintedIndex + 1);
lineIndex <= endContext;
lineIndex++
) {
const diffLine = diff[lineIndex];
if (diffLine.type === "added") {
lines.push(`${GREEN}+ ${diffLine.content}${RESET}`);
hasChanges = true;
} else if (diffLine.type === "removed") {
lines.push(`${RED}- ${diffLine.content}${RESET}`);
hasChanges = true;
} else {
lines.push(`${GRAY} ${diffLine.content}${RESET}`);
}
lastPrintedIndex = lineIndex;
}
}
return hasChanges ? lines.join("\n") : `${GRAY}No changes${RESET}`;
};
export const printDiff = (
filePath: string,
originalContent: string,
newContent: string,
): void => {
console.log(`\n${BOLD}File: ${filePath}${RESET}`);
console.log("─".repeat(60));
const diff = generateDiff(originalContent, newContent);
console.log(formatDiff(diff));
console.log("─".repeat(60));
};
================================================
FILE: packages/cli/src/utils/handle-error.ts
================================================
import { logger } from "./logger.js";
export const handleError = (error: unknown) => {
logger.break();
logger.error(
"Something went wrong. Please check the error below for more details.",
);
logger.error("If the problem persists, please open an issue on GitHub.");
logger.error("");
if (error instanceof Error) {
logger.error(error.message);
}
logger.break();
process.exit(1);
};
================================================
FILE: packages/cli/src/utils/highlighter.ts
================================================
import pc from "picocolors";
export const highlighter = {
error: pc.red,
warn: pc.yellow,
info: pc.cyan,
success: pc.green,
dim: pc.dim,
};
================================================
FILE: packages/cli/src/utils/install-mcp.ts
================================================
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import * as jsonc from "jsonc-parser";
import * as TOML from "smol-toml";
import { highlighter } from "./highlighter.js";
import { logger } from "./logger.js";
import { prompts } from "./prompts.js";
import { spinner } from "./spinner.js";
const SERVER_NAME = "react-grab-mcp";
const PACKAGE_NAME = "@react-grab/mcp";
export interface ClientDefinition {
name: string;
configPath: string;
configKey: string;
format: "json" | "toml";
serverConfig: Record<string, unknown>;
}
interface InstallResult {
client: string;
configPath: string;
success: boolean;
error?: string;
}
const getXdgConfigHome = (): string =>
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
const getBaseDir = (): string => {
const homeDir = os.homedir();
if (process.platform === "win32") {
return process.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
}
if (process.platform === "darwin") {
return path.join(homeDir, "Library", "Application Support");
}
return getXdgConfigHome();
};
const getZedConfigPath = (): string => {
if (process.platform === "win32") {
return path.join(getBaseDir(), "Zed", "settings.json");
}
return path.join(os.homedir(), ".config", "zed", "settings.json");
};
export const getOpenCodeConfigPath = (): string => {
const configDir = path.join(getXdgConfigHome(), "opencode");
const jsoncPath = path.join(configDir, "opencode.jsonc");
const jsonPath = path.join(configDir, "opencode.json");
if (fs.existsSync(jsoncPath)) return jsoncPath;
if (fs.existsSync(jsonPath)) return jsonPath;
return jsoncPath;
};
const getClients = (): ClientDefinition[] => {
const homeDir = os.homedir();
const baseDir = getBaseDir();
const stdioConfig = {
command: "npx",
args: ["-y", PACKAGE_NAME, "--stdio"],
};
return [
{
name: "Claude Code",
configPath: path.join(homeDir, ".claude.json"),
configKey: "mcpServers",
format: "json",
serverConfig: stdioConfig,
},
{
name: "Codex",
configPath: path.join(
process.env.CODEX_HOME || path.join(homeDir, ".codex"),
"config.toml",
),
configKey: "mcp_servers",
format: "toml",
serverConfig: stdioConfig,
},
{
name: "Cursor",
configPath: path.join(homeDir, ".cursor", "mcp.json"),
configKey: "mcpServers",
format: "json",
serverConfig: stdioConfig,
},
{
name: "OpenCode",
configPath: getOpenCodeConfigPath(),
configKey: "mcp",
format: "json",
serverConfig: {
type: "local",
command: ["npx", "-y", PACKAGE_NAME, "--stdio"],
},
},
{
name: "VS Code",
configPath: path.join(baseDir, "Code", "User", "mcp.json"),
configKey: "servers",
format: "json",
serverConfig: { type: "stdio", ...stdioConfig },
},
{
name: "Amp",
configPath: path.join(homeDir, ".config", "amp", "settings.json"),
configKey: "amp.mcpServers",
format: "json",
serverConfig: stdioConfig,
},
{
name: "Droid",
configPath: path.join(homeDir, ".factory", "mcp.json"),
configKey: "mcpServers",
format: "json",
serverConfig: { type: "stdio", ...stdioConfig },
},
{
name: "Windsurf",
configPath: path.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
configKey: "mcpServers",
format: "json",
serverConfig: stdioConfig,
},
{
name: "Zed",
configPath: getZedConfigPath(),
configKey: "context_servers",
format: "json",
serverConfig: { source: "custom", ...stdioConfig, env: {} },
},
];
};
const ensureDirectory = (filePath: string): void => {
const directory = path.dirname(filePath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
};
const JSONC_FORMAT_OPTIONS: jsonc.FormattingOptions = {
tabSize: 2,
insertSpaces: true,
};
export const upsertIntoJsonc = (
filePath: string,
content: string,
configKey: string,
serverName: string,
serverConfig: Record<string, unknown>,
): void => {
const edits = jsonc.modify(content, [configKey, serverName], serverConfig, {
formattingOptions: JSONC_FORMAT_OPTIONS,
});
fs.writeFileSync(filePath, jsonc.applyEdits(content, edits));
};
export const installJsonClient = (client: ClientDefinition): void => {
ensureDirectory(client.configPath);
const content = fs.existsSync(client.configPath)
? fs.readFileSync(client.configPath, "utf8")
: "{}";
upsertIntoJsonc(
client.configPath,
content,
client.configKey,
SERVER_NAME,
client.serverConfig,
);
};
export const installTomlClient = (client: ClientDefinition): void => {
ensureDirectory(client.configPath);
const existingConfig: Record<string, unknown> = fs.existsSync(
client.configPath,
)
? TOML.parse(fs.readFileSync(client.configPath, "utf8"))
: {};
const serverSection = (existingConfig[client.configKey] ?? {}) as Record<
string,
unknown
>;
serverSection[SERVER_NAME] = client.serverConfig;
existingConfig[client.configKey] = serverSection;
fs.writeFileSync(client.configPath, TOML.stringify(existingConfig));
};
export const getMcpClientNames = (): string[] =>
getClients().map((client) => client.name);
export const installMcpServers = (
selectedClients?: string[],
): InstallResult[] => {
const allClients = getClients();
const clients = selectedClients
? allClients.filter((client) => selectedClients.includes(client.name))
: allClients;
const results: InstallResult[] = [];
const installSpinner = spinner("Installing MCP server.").start();
for (const client of clients) {
try {
if (client.format === "toml") {
installTomlClient(client);
} else {
installJsonClient(client);
}
results.push({
client: client.name,
configPath: client.configPath,
success: true,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
results.push({
client: client.name,
configPath: client.configPath,
success: false,
error: message,
});
}
}
const successCount = results.filter((result) => result.success).length;
if (successCount < results.length) {
installSpinner.warn(
`Installed to ${successCount}/${results.length} agents.`,
);
} else {
installSpinner.succeed(`Installed to ${successCount} agents.`);
}
for (const result of results) {
if (result.success) {
logger.log(
` ${highlighter.success("\u2713")} ${result.client} ${highlighter.dim("\u2192")} ${highlighter.dim(result.configPath)}`,
);
} else {
logger.log(
` ${highlighter.error("\u2717")} ${result.client} ${highlighter.dim("\u2192")} ${result.error}`,
);
}
}
return results;
};
export const promptConnectionMode = async (): Promise<
"mcp" | "legacy" | undefined
> => {
const { connectionMode } = await prompts({
type: "select",
name: "connectionMode",
message: "How would you like to connect?",
choices: [
{
title: `MCP ${highlighter.dim("(recommended)")}`,
description: "Installs to all supported agents at once",
value: "mcp",
},
{
title: "Legacy",
description: "Install a per-project agent package",
value: "legacy",
},
],
});
return connectionMode as "mcp" | "legacy" | undefined;
};
export const promptMcpInstall = async (): Promise<boolean> => {
const clientNames = getMcpClientNames();
const { selectedAgents } = await prompts({
type: "multiselect",
name: "selectedAgents",
message: "Select agents to install MCP server for:",
choices: clientNames.map((name) => ({
title: name,
value: name,
selected: true,
})),
});
if (selectedAgents === undefined || selectedAgents.length === 0) {
return false;
}
logger.break();
const results = installMcpServers(selectedAgents);
const hasSuccess = results.some((result) => result.success);
return hasSuccess;
};
================================================
FILE: packages/cli/src/utils/install.ts
================================================
import { execSync } from "node:child_process";
import type { PackageManager } from "./detect.js";
import type { AgentIntegration } from "./templates.js";
const INSTALL_COMMANDS: Record<PackageManager, string> = {
npm: "npm install",
yarn: "yarn add",
pnpm: "pnpm add",
bun: "bun add",
};
const UNINSTALL_COMMANDS: Record<PackageManager, string> = {
npm: "npm uninstall",
yarn: "yarn remove",
pnpm: "pnpm remove",
bun: "bun remove",
};
export const installPackages = (
packages: string[],
packageManager: PackageManager,
projectRoot: string,
isDev: boolean = true,
): void => {
if (packages.length === 0) {
return;
}
const command = INSTALL_COMMANDS[packageManager];
const devFlag = isDev ? " -D" : "";
const fullCommand = `${command}${devFlag} ${packages.join(" ")}`;
console.log(`Running: ${fullCommand}\n`);
execSync(fullCommand, {
cwd: projectRoot,
stdio: "inherit",
env: { ...process.env, REACT_GRAB_INIT: "1" },
});
};
export const getPackagesToInstall = (
agent: AgentIntegration,
includeReactGrab: boolean = true,
): string[] => {
const packages: string[] = [];
if (includeReactGrab) {
packages.push("react-grab");
}
if (agent !== "none") {
packages.push(`@react-grab/${agent}`);
}
return packages;
};
export const uninstallPackages = (
packages: string[],
packageManager: PackageManager,
projectRoot: string,
): void => {
if (packages.length === 0) {
return;
}
const command = UNINSTALL_COMMANDS[packageManager];
const fullCommand = `${command} ${packages.join(" ")}`;
console.log(`Running: ${fullCommand}\n`);
execSync(fullCommand, {
cwd: projectRoot,
stdio: "inherit",
});
};
export const getPackagesToUninstall = (agent: string): string[] => {
return [`@react-grab/${agent}`];
};
================================================
FILE: packages/cli/src/utils/is-non-interactive.ts
================================================
const AGENT_ENVIRONMENT_VARIABLES = [
"CI",
"CLAUDECODE",
"CURSOR_AGENT",
"CODEX_CI",
"OPENCODE",
"AMP_HOME",
"AMI",
] as const;
const isEnvironmentVariableSet = (variable: string): boolean =>
Boolean(process.env[variable]);
export const detectNonInteractive = (yesFlag: boolean): boolean =>
yesFlag ||
AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) ||
!process.stdin.isTTY;
================================================
FILE: packages/cli/src/utils/logger.ts
================================================
import { highlighter } from "./highlighter.js";
export const logger = {
error(...args: unknown[]) {
console.log(highlighter.error(args.join(" ")));
},
warn(...args: unknown[]) {
console.log(highlighter.warn(args.join(" ")));
},
info(...args: unknown[]) {
console.log(highlighter.info(args.join(" ")));
},
success(...args: unknown[]) {
console.log(highlighter.success(args.join(" ")));
},
dim(...args: unknown[]) {
console.log(highlighter.dim(args.join(" ")));
},
log(...args: unknown[]) {
console.log(args.join(" "));
},
break() {
console.log("");
},
};
================================================
FILE: packages/cli/src/utils/prompts.ts
================================================
import basePrompts, { type PromptObject, type Answers } from "prompts";
import { logger } from "./logger.js";
const onCancel = () => {
logger.break();
logger.log("Cancelled.");
logger.break();
process.exit(0);
};
export const prompts = <T extends string = string>(
questions: PromptObject<T> | PromptObject<T>[],
): Promise<Answers<T>> => {
return basePrompts(questions, { onCancel });
};
================================================
FILE: packages/cli/src/utils/spinner.ts
================================================
import ora from "ora";
interface SpinnerOptions {
silent?: boolean;
}
export const spinner = (text: string, options?: SpinnerOptions) =>
ora({ text, isSilent: options?.silent });
================================================
FILE: packages/cli/src/utils/templates.ts
================================================
export const AGENTS = [
"claude-code",
"cursor",
"opencode",
"codex",
"gemini",
"amp",
"droid",
"copilot",
] as const;
export type Agent = (typeof AGENTS)[number];
export type AgentIntegration = Agent | "mcp" | "none";
export const AGENT_NAMES: Record<Agent, string> = {
"claude-code": "Claude Code",
cursor: "Cursor",
opencode: "OpenCode",
codex: "Codex",
gemini: "Gemini",
amp: "Amp",
droid: "Droid",
copilot: "Copilot",
};
export const getAgentDisplayName = (agent: string): string => {
if (agent === "mcp") return "MCP";
if (agent in AGENT_NAMES) {
return AGENT_NAMES[agent as Agent];
}
return agent;
};
export const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}`;
export const NEXT_APP_ROUTER_SCRIPT_WITH_AGENT = (
agent: AgentIntegration,
): string => {
if (agent === "none") return NEXT_APP_ROUTER_SCRIPT;
return `{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
strategy="lazyOnload"
/>
)}`;
};
export const NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}`;
export const NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT = (
agent: AgentIntegration,
): string => {
if (agent === "none") return NEXT_PAGES_ROUTER_SCRIPT;
return `{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
strategy="lazyOnload"
/>
)}`;
};
export const VITE_IMPORT = `if (import.meta.env.DEV) {
import("react-grab");
}`;
export const VITE_IMPORT_WITH_AGENT = (agent: AgentIntegration): string => {
if (agent === "none") return VITE_IMPORT;
return `if (import.meta.env.DEV) {
import("react-grab");
import("@react-grab/${agent}/client");
}`;
};
export const WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
import("react-grab");
}`;
export const WEBPACK_IMPORT_WITH_AGENT = (agent: AgentIntegration): string => {
if (agent === "none") return WEBPACK_IMPORT;
return `if (process.env.NODE_ENV === "development") {
import("react-grab");
import("@react-grab/${agent}/client");
}`;
};
export const TANSTACK_EFFECT = `useEffect(() => {
if (import.meta.env.DEV) {
void import("react-grab");
}
}, []);`;
export const TANSTACK_EFFECT_WITH_AGENT = (agent: AgentIntegration): string => {
if (agent === "none") return TANSTACK_EFFECT;
return `useEffect(() => {
if (import.meta.env.DEV) {
void import("react-grab");
void import("@react-grab/${agent}/client");
}
}, []);`;
};
export const SCRIPT_IMPORT = 'import Script from "next/script";';
================================================
FILE: packages/cli/src/utils/transform.ts
================================================
import {
accessSync,
constants,
existsSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import type { Framework, NextRouterType, PackageManager } from "./detect.js";
import {
NEXT_APP_ROUTER_SCRIPT_WITH_AGENT,
NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT,
SCRIPT_IMPORT,
TANSTACK_EFFECT_WITH_AGENT,
VITE_IMPORT_WITH_AGENT,
WEBPACK_IMPORT_WITH_AGENT,
type AgentIntegration,
} from "./templates.js";
export interface TransformResult {
success: boolean;
filePath: string;
message: string;
originalContent?: string;
newContent?: string;
noChanges?: boolean;
}
export interface ReactGrabOptions {
activationKey?: string;
activationMode?: "toggle" | "hold";
keyHoldDuration?: number;
allowActivationInsideInput?: boolean;
maxContextLines?: number;
}
export interface PackageJsonTransformResult {
success: boolean;
filePath: string;
message: string;
originalContent?: string;
newContent?: string;
noChanges?: boolean;
warning?: string;
}
const hasReactGrabCode = (content: string): boolean => {
const fuzzyPatterns = [
/["'`][^"'`]*react-grab/,
/react-grab[^"'`]*["'`]/,
/<[^>]*react-grab/i,
/import[^;]*react-grab/i,
/require[^)]*react-grab/i,
/from\s+[^;]*react-grab/i,
/src[^>]*react-grab/i,
/href[^>]*react-grab/i,
];
return fuzzyPatterns.some((pattern) => pattern.test(content));
};
const findLayoutFile = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "app", "layout.tsx"),
join(projectRoot, "app", "layout.jsx"),
join(projectRoot, "src", "app", "layout.tsx"),
join(projectRoot, "src", "app", "layout.jsx"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const findInstrumentationFile = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "instrumentation-client.ts"),
join(projectRoot, "instrumentation-client.js"),
join(projectRoot, "src", "instrumentation-client.ts"),
join(projectRoot, "src", "instrumentation-client.js"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const hasReactGrabInInstrumentation = (projectRoot: string): boolean => {
const instrumentationPath = findInstrumentationFile(projectRoot);
if (!instrumentationPath) return false;
const content = readFileSync(instrumentationPath, "utf-8");
return hasReactGrabCode(content);
};
const findDocumentFile = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "pages", "_document.tsx"),
join(projectRoot, "pages", "_document.jsx"),
join(projectRoot, "src", "pages", "_document.tsx"),
join(projectRoot, "src", "pages", "_document.jsx"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const findIndexHtml = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "index.html"),
join(projectRoot, "public", "index.html"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const findEntryFile = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "src", "index.tsx"),
join(projectRoot, "src", "index.jsx"),
join(projectRoot, "src", "index.ts"),
join(projectRoot, "src", "index.js"),
join(projectRoot, "src", "main.tsx"),
join(projectRoot, "src", "main.jsx"),
join(projectRoot, "src", "main.ts"),
join(projectRoot, "src", "main.js"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const findTanStackRootFile = (projectRoot: string): string | null => {
const possiblePaths = [
join(projectRoot, "src", "routes", "__root.tsx"),
join(projectRoot, "src", "routes", "__root.jsx"),
join(projectRoot, "app", "routes", "__root.tsx"),
join(projectRoot, "app", "routes", "__root.jsx"),
];
for (const filePath of possiblePaths) {
if (existsSync(filePath)) {
return filePath;
}
}
return null;
};
const addAgentToExistingNextApp = (
originalContent: string,
agent: AgentIntegration,
filePath: string,
): TransformResult => {
if (agent === "none") {
return {
success: true,
filePath,
message: "React Grab is already configured",
noChanges: true,
};
}
const agentPackage = `@react-grab/${agent}`;
if (originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is already configured`,
noChanges: true,
};
}
const agentScript = `{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/${agentPackage}/dist/client.global.js"
strategy="lazyOnload"
/>
)}`;
const reactGrabBlockMatch = originalContent.match(
/\{process\.env\.NODE_ENV\s*===\s*["']development["']\s*&&\s*\(\s*<Script[^>]*react-grab[^>]*\/>\s*\)\}/is,
);
if (reactGrabBlockMatch) {
const newContent = originalContent.replace(
reactGrabBlockMatch[0],
`${reactGrabBlockMatch[0]}\n ${agentScript}`,
);
return {
success: true,
filePath,
message: `Add ${agent} agent`,
originalContent,
newContent,
};
}
const bareScriptMatch = originalContent.match(
/<Script[^>]*react-grab[^>]*\/>/i,
);
if (bareScriptMatch) {
const newContent = originalContent.replace(
bareScriptMatch[0],
`${bareScriptMatch[0]}\n <Script src="//unpkg.com/${agentPackage}/dist/client.global.js" strategy="lazyOnload" />`,
);
return {
success: true,
filePath,
message: `Add ${agent} agent`,
originalContent,
newContent,
};
}
return {
success: false,
filePath,
message: "Could not find React Grab script to add agent after",
};
};
const addAgentToExistingImport = (
originalContent: string,
agent: AgentIntegration,
filePath: string,
): TransformResult => {
if (agent === "none") {
return {
success: true,
filePath,
message: "React Grab is already configured",
noChanges: true,
};
}
const agentPackage = `@react-grab/${agent}`;
if (originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is already configured`,
noChanges: true,
};
}
const agentImport = `import("${agentPackage}/client");`;
const reactGrabImportMatch = originalContent.match(
/import\s*\(\s*["']react-grab["']\s*\);?/,
);
if (reactGrabImportMatch) {
const matchedText = reactGrabImportMatch[0];
const hasSemicolon = matchedText.endsWith(";");
const newContent = originalContent.replace(
matchedText,
`${hasSemicolon ? matchedText.slice(0, -1) : matchedText};\n ${agentImport}`,
);
return {
success: true,
filePath,
message: `Add ${agent} agent`,
originalContent,
newContent,
};
}
return {
success: false,
filePath,
message: "Could not find React Grab import to add agent after",
};
};
const addAgentToExistingTanStack = (
originalContent: string,
agent: AgentIntegration,
filePath: string,
): TransformResult => {
if (agent === "none") {
return {
success: true,
filePath,
message: "React Grab is already configured",
noChanges: true,
};
}
const agentPackage = `@react-grab/${agent}`;
if (originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is already configured`,
noChanges: true,
};
}
const agentImport = `void import("${agentPackage}/client");`;
const reactGrabImportMatch = originalContent.match(
/void\s+import\s*\(\s*["']react-grab["']\s*\);?/,
);
if (reactGrabImportMatch) {
const matchedText = reactGrabImportMatch[0];
const hasSemicolon = matchedText.endsWith(";");
const newContent = originalContent.replace(
matchedText,
`${hasSemicolon ? matchedText.slice(0, -1) : matchedText};\n ${agentImport}`,
);
return {
success: true,
filePath,
message: `Add ${agent} agent`,
originalContent,
newContent,
};
}
return {
success: false,
filePath,
message: "Could not find React Grab import to add agent after",
};
};
const transformNextAppRouter = (
projectRoot: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
force: boolean = false,
): TransformResult => {
const layoutPath = findLayoutFile(projectRoot);
if (!layoutPath) {
return {
success: false,
filePath: "",
message: "Could not find app/layout.tsx or app/layout.jsx",
};
}
const originalContent = readFileSync(layoutPath, "utf-8");
let newContent = originalContent;
const hasReactGrabInFile = hasReactGrabCode(originalContent);
const hasReactGrabInInstrumentationFile =
hasReactGrabInInstrumentation(projectRoot);
if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {
return addAgentToExistingNextApp(originalContent, agent, layoutPath);
}
if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) {
return {
success: true,
filePath: layoutPath,
message:
"React Grab is already installed" +
(hasReactGrabInInstrumentationFile
? " in instrumentation-client"
: " in this file"),
noChanges: true,
};
}
if (!newContent.includes('import Script from "next/script"')) {
const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
if (importMatch) {
newContent = newContent.replace(
importMatch[0],
`${importMatch[0]}\n${SCRIPT_IMPORT}`,
);
} else {
newContent = `${SCRIPT_IMPORT}\n\n${newContent}`;
}
}
const scriptBlock = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);
const headMatch = newContent.match(/<head[^>]*>/);
if (headMatch) {
newContent = newContent.replace(
headMatch[0],
`${headMatch[0]}\n ${scriptBlock}`,
);
} else {
const htmlMatch = newContent.match(/<html[^>]*>/);
if (htmlMatch) {
newContent = newContent.replace(
htmlMatch[0],
`${htmlMatch[0]}\n <head>\n ${scriptBlock}\n </head>`,
);
}
}
return {
success: true,
filePath: layoutPath,
message:
"Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
originalContent,
newContent,
};
};
const transformNextPagesRouter = (
projectRoot: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
force: boolean = false,
): TransformResult => {
const documentPath = findDocumentFile(projectRoot);
if (!documentPath) {
return {
success: false,
filePath: "",
message:
"Could not find pages/_document.tsx or pages/_document.jsx.\n\n" +
"To set up React Grab with Pages Router, create pages/_document.tsx with:\n\n" +
' import { Html, Head, Main, NextScript } from "next/document";\n' +
' import Script from "next/script";\n\n' +
" export default function Document() {\n" +
" return (\n" +
" <Html>\n" +
" <Head>\n" +
' {process.env.NODE_ENV === "development" && (\n' +
' <Script src="//unpkg.com/react-grab/dist/index.global.js" strategy="beforeInteractive" />\n' +
" )}\n" +
" </Head>\n" +
" <body>\n" +
" <Main />\n" +
" <NextScript />\n" +
" </body>\n" +
" </Html>\n" +
" );\n" +
" }",
};
}
const originalContent = readFileSync(documentPath, "utf-8");
let newContent = originalContent;
const hasReactGrabInFile = hasReactGrabCode(originalContent);
const hasReactGrabInInstrumentationFile =
hasReactGrabInInstrumentation(projectRoot);
if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {
return addAgentToExistingNextApp(originalContent, agent, documentPath);
}
if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) {
return {
success: true,
filePath: documentPath,
message:
"React Grab is already installed" +
(hasReactGrabInInstrumentationFile
? " in instrumentation-client"
: " in this file"),
noChanges: true,
};
}
if (!newContent.includes('import Script from "next/script"')) {
const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
if (importMatch) {
newContent = newContent.replace(
importMatch[0],
`${importMatch[0]}\n${SCRIPT_IMPORT}`,
);
}
}
const scriptBlock = NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT(agent);
const headMatch = newContent.match(/<Head[^>]*>/);
if (headMatch) {
newContent = newContent.replace(
headMatch[0],
`${headMatch[0]}\n ${scriptBlock}`,
);
}
return {
success: true,
filePath: documentPath,
message:
"Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
originalContent,
newContent,
};
};
const checkExistingInstallation = (
filePath: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
): TransformResult | null => {
const content = readFileSync(filePath, "utf-8");
if (!hasReactGrabCode(content)) return null;
if (reactGrabAlreadyConfigured) {
return addAgentToExistingImport(content, agent, filePath);
}
return {
success: true,
filePath,
message: "React Grab is already installed in this file",
noChanges: true,
};
};
const transformVite = (
projectRoot: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
force: boolean = false,
): TransformResult => {
const entryPath = findEntryFile(projectRoot);
if (!force) {
const indexPath = findIndexHtml(projectRoot);
if (indexPath) {
const existingResult = checkExistingInstallation(
indexPath,
agent,
reactGrabAlreadyConfigured,
);
if (existingResult) return existingResult;
}
}
if (!entryPath) {
return {
success: false,
filePath: "",
message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)",
};
}
if (!force) {
const existingResult = checkExistingInstallation(
entryPath,
agent,
reactGrabAlreadyConfigured,
);
if (existingResult) return existingResult;
}
const originalContent = readFileSync(entryPath, "utf-8");
const importBlock = VITE_IMPORT_WITH_AGENT(agent);
const newContent = `${importBlock}\n\n${originalContent}`;
return {
success: true,
filePath: entryPath,
message:
"Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
originalContent,
newContent,
};
};
const transformWebpack = (
projectRoot: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
force: boolean = false,
): TransformResult => {
const entryPath = findEntryFile(projectRoot);
if (!entryPath) {
return {
success: false,
filePath: "",
message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)",
};
}
if (!force) {
const existingResult = checkExistingInstallation(
entryPath,
agent,
reactGrabAlreadyConfigured,
);
if (existingResult) return existingResult;
}
const originalContent = readFileSync(entryPath, "utf-8");
const importBlock = WEBPACK_IMPORT_WITH_AGENT(agent);
const newContent = `${importBlock}\n\n${originalContent}`;
return {
success: true,
filePath: entryPath,
message:
"Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
originalContent,
newContent,
};
};
const transformTanStack = (
projectRoot: string,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean,
force: boolean = false,
): TransformResult => {
const rootPath = findTanStackRootFile(projectRoot);
if (!rootPath) {
return {
success: false,
filePath: "",
message:
"Could not find src/routes/__root.tsx or app/routes/__root.tsx.\n\n" +
"To set up React Grab with TanStack Start, add this to your root route component:\n\n" +
' import { useEffect } from "react";\n\n' +
" useEffect(() => {\n" +
" if (import.meta.env.DEV) {\n" +
' void import("react-grab");\n' +
" }\n" +
" }, []);",
};
}
const originalContent = readFileSync(rootPath, "utf-8");
let newContent = originalContent;
const hasReactGrabInFile = hasReactGrabCode(originalContent);
if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) {
return addAgentToExistingTanStack(originalContent, agent, rootPath);
}
if (!force && hasReactGrabInFile) {
return {
success: true,
filePath: rootPath,
message: "React Grab is already installed in this file",
noChanges: true,
};
}
const hasUseEffectImport =
/import\s+\{[^}]*useEffect[^}]*\}\s+from\s+["']react["']/.test(newContent);
if (!hasUseEffectImport) {
const reactImportMatch = newContent.match(
/import\s+\{([^}]*)\}\s+from\s+["']react["'];?/,
);
if (reactImportMatch) {
const existingImports = reactImportMatch[1];
newContent = newContent.replace(
reactImportMatch[0],
`import { ${existingImports.trim()}, useEffect } from "react";`,
);
} else {
const firstImportMatch = newContent.match(
/^import .+ from ['"].+['"];?\s*$/m,
);
if (firstImportMatch) {
newContent = newContent.replace(
firstImportMatch[0],
`import { useEffect } from "react";\n${firstImportMatch[0]}`,
);
} else {
newContent = `import { useEffect } from "react";\n\n${newContent}`;
}
}
}
const effectBlock = TANSTACK_EFFECT_WITH_AGENT(agent);
const componentMatch = newContent.match(/function\s+(\w+)\s*\([^)]*\)\s*\{/);
if (componentMatch) {
const insertPosition = componentMatch.index! + componentMatch[0].length;
newContent =
newContent.slice(0, insertPosition) +
`\n ${effectBlock}\n` +
newContent.slice(insertPosition);
} else {
return {
success: false,
filePath: rootPath,
message: "Could not find a component function in the root file",
};
}
return {
success: true,
filePath: rootPath,
message:
"Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
originalContent,
newContent,
};
};
export const previewTransform = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean = false,
force: boolean = false,
): TransformResult => {
const resolvedAgent: AgentIntegration = agent === "mcp" ? "none" : agent;
switch (framework) {
case "next":
if (nextRouterType === "app") {
return transformNextAppRouter(
projectRoot,
resolvedAgent,
reactGrabAlreadyConfigured,
force,
);
}
return transformNextPagesRouter(
projectRoot,
resolvedAgent,
reactGrabAlreadyConfigured,
force,
);
case "vite":
return transformVite(
projectRoot,
resolvedAgent,
reactGrabAlreadyConfigured,
force,
);
case "tanstack":
return transformTanStack(
projectRoot,
resolvedAgent,
reactGrabAlreadyConfigured,
force,
);
case "webpack":
return transformWebpack(
projectRoot,
resolvedAgent,
reactGrabAlreadyConfigured,
force,
);
default:
return {
success: false,
filePath: "",
message: `Unknown framework: ${framework}. Please add React Grab manually.`,
};
}
};
const canWriteToFile = (filePath: string): boolean => {
try {
accessSync(filePath, constants.W_OK);
return true;
} catch {
return false;
}
};
export const applyTransform = (
result: TransformResult,
): { success: boolean; error?: string } => {
if (result.success && result.newContent && result.filePath) {
if (!canWriteToFile(result.filePath)) {
return {
success: false,
error: `Cannot write to ${result.filePath}. Check file permissions.`,
};
}
try {
writeFileSync(result.filePath, result.newContent);
return { success: true };
} catch (error) {
return {
success: false,
error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
return { success: true };
};
export const transformProject = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
agent: AgentIntegration,
reactGrabAlreadyConfigured: boolean = false,
): TransformResult & { writeError?: string } => {
const result = previewTransform(
projectRoot,
framework,
nextRouterType,
agent,
reactGrabAlreadyConfigured,
);
const writeResult = applyTransform(result);
if (!writeResult.success) {
return { ...result, success: false, writeError: writeResult.error };
}
return result;
};
const getPackageExecutor = (packageManager: PackageManager): string => {
switch (packageManager) {
case "bun":
return "bunx";
case "pnpm":
return "pnpm dlx";
case "yarn":
return "npx";
case "npm":
default:
return "npx";
}
};
const AGENT_PACKAGES: Record<string, string> = {
"claude-code": "@react-grab/claude-code@latest",
cursor: "@react-grab/cursor@latest",
opencode: "@react-grab/opencode@latest",
codex: "@react-grab/codex@latest",
gemini: "@react-grab/gemini@latest",
amp: "@react-grab/amp@latest",
droid: "@react-grab/droid@latest",
copilot: "@react-grab/copilot@latest",
};
export const getAgentPrefix = (
agent: string,
packageManager: PackageManager,
): string | null => {
const agentPackage = AGENT_PACKAGES[agent];
if (!agentPackage) return null;
const executor = getPackageExecutor(packageManager);
return `${executor} ${agentPackage} &&`;
};
const getAllAgentPrefixVariants = (agent: string): string[] => {
const agentPackage = AGENT_PACKAGES[agent];
if (!agentPackage) return [];
return [
`npx ${agentPackage} &&`,
`bunx ${agentPackage} &&`,
`pnpm dlx ${agentPackage} &&`,
`yarn dlx ${agentPackage} &&`,
];
};
export const previewPackageJsonTransform = (
projectRoot: string,
agent: AgentIntegration,
installedAgents: string[],
packageManager: PackageManager = "npm",
): PackageJsonTransformResult => {
if (agent === "none") {
return {
success: true,
filePath: "",
message: "No agent selected, skipping package.json modification",
noChanges: true,
};
}
if (agent === "mcp") {
return {
success: true,
filePath: "",
message: "MCP does not use package.json dev script",
noChanges: true,
};
}
const packageJsonPath = join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return {
success: false,
filePath: "",
message: "Could not find package.json",
};
}
const originalContent = readFileSync(packageJsonPath, "utf-8");
const agentPrefix = getAgentPrefix(agent, packageManager);
if (!agentPrefix) {
return {
success: false,
filePath: packageJsonPath,
message: `Unknown agent: ${agent}`,
};
}
const allPrefixVariants = getAllAgentPrefixVariants(agent);
const hasExistingPrefix = allPrefixVariants.some((prefix) =>
originalContent.includes(prefix),
);
if (hasExistingPrefix) {
return {
success: true,
filePath: packageJsonPath,
message: `Agent ${agent} dev script is already configured`,
noChanges: true,
};
}
try {
const packageJson = JSON.parse(originalContent);
let targetScriptKey = "dev";
if (!packageJson.scripts?.dev) {
const devScriptKeys = Object.keys(packageJson.scripts || {}).filter(
(key) => key.startsWith("dev"),
);
if (devScriptKeys.length > 0) {
targetScriptKey = devScriptKeys[0];
} else {
return {
success: true,
filePath: packageJsonPath,
message: "No dev script found in package.json",
noChanges: true,
warning: `Could not inject agent into package.json (no dev script found).\nRun this command manually before starting your dev server:\n ${agentPrefix} <your dev command>`,
};
}
}
const currentDevScript = packageJson.scripts[targetScriptKey];
for (const installedAgent of installedAgents) {
const installedPrefixVariants = getAllAgentPrefixVariants(installedAgent);
const hasInstalledAgentPrefix = installedPrefixVariants.some((prefix) =>
currentDevScript.includes(prefix),
);
if (hasInstalledAgentPrefix) {
return {
success: true,
filePath: packageJsonPath,
message: `Agent ${installedAgent} is already in ${targetScriptKey} script`,
noChanges: true,
};
}
}
packageJson.scripts[targetScriptKey] = `${agentPrefix} ${currentDevScript}`;
const newContent = JSON.stringify(packageJson, null, 2) + "\n";
return {
success: true,
filePath: packageJsonPath,
message: `Add ${agent} server to ${targetScriptKey} script`,
originalContent,
newContent,
};
} catch {
return {
success: false,
filePath: packageJsonPath,
message: "Failed to parse package.json",
};
}
};
export const applyPackageJsonTransform = (
result: PackageJsonTransformResult,
): { success: boolean; error?: string } => {
if (result.success && result.newContent && result.filePath) {
if (!canWriteToFile(result.filePath)) {
return {
success: false,
error: `Cannot write to ${result.filePath}. Check file permissions.`,
};
}
try {
writeFileSync(result.filePath, result.newContent);
return { success: true };
} catch (error) {
return {
success: false,
error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
return { success: true };
};
const formatOptionsForNextjs = (options: ReactGrabOptions): string => {
const parts: string[] = [];
if (options.activationKey) {
parts.push(`activationKey: ${JSON.stringify(options.activationKey)}`);
}
if (options.activationMode) {
parts.push(`activationMode: "${options.activationMode}"`);
}
if (options.keyHoldDuration !== undefined) {
parts.push(`keyHoldDuration: ${options.keyHoldDuration}`);
}
if (options.allowActivationInsideInput !== undefined) {
parts.push(
`allowActivationInsideInput: ${options.allowActivationInsideInput}`,
);
}
if (options.maxContextLines !== undefined) {
parts.push(`maxContextLines: ${options.maxContextLines}`);
}
return `{ ${parts.join(", ")} }`;
};
const formatOptionsAsJson = (options: ReactGrabOptions): string => {
const cleanOptions: Record<string, unknown> = {};
if (options.activationKey) {
cleanOptions.activationKey = options.activationKey;
}
if (options.activationMode) {
cleanOptions.activationMode = options.activationMode;
}
if (options.keyHoldDuration !== undefined) {
cleanOptions.keyHoldDuration = options.keyHoldDuration;
}
if (options.allowActivationInsideInput !== undefined) {
cleanOptions.allowActivationInsideInput =
options.allowActivationInsideInput;
}
if (options.maxContextLines !== undefined) {
cleanOptions.maxContextLines = options.maxContextLines;
}
return JSON.stringify(cleanOptions);
};
const findReactGrabFile = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
): string | null => {
switch (framework) {
case "next":
if (nextRouterType === "app") {
return findLayoutFile(projectRoot);
}
return findDocumentFile(projectRoot);
case "vite": {
const entryFile = findEntryFile(projectRoot);
if (entryFile && hasReactGrabCode(readFileSync(entryFile, "utf-8"))) {
return entryFile;
}
const indexHtml = findIndexHtml(projectRoot);
if (indexHtml && hasReactGrabCode(readFileSync(indexHtml, "utf-8"))) {
return indexHtml;
}
return entryFile;
}
case "tanstack":
return findTanStackRootFile(projectRoot);
case "webpack":
return findEntryFile(projectRoot);
default:
return null;
}
};
const addOptionsToNextScript = (
originalContent: string,
options: ReactGrabOptions,
filePath: string,
): TransformResult => {
const reactGrabScriptMatch = originalContent.match(
/(<Script[\s\S]*?react-grab[\s\S]*?)\s*(\/?>)/i,
);
if (!reactGrabScriptMatch) {
return {
success: false,
filePath,
message: "Could not find React Grab Script tag",
};
}
const scriptTag = reactGrabScriptMatch[0];
const scriptOpening = reactGrabScriptMatch[1];
const scriptClosing = reactGrabScriptMatch[2];
const existingDataOptionsMatch = scriptTag.match(
/data-options=\{JSON\.stringify\([^)]+\)\}/,
);
const dataOptionsAttr = `data-options={JSON.stringify(\n ${formatOptionsForNextjs(options)}\n )}`;
let newScriptTag: string;
if (existingDataOptionsMatch) {
newScriptTag = scriptTag.replace(
existingDataOptionsMatch[0],
dataOptionsAttr,
);
} else {
newScriptTag = `${scriptOpening}\n ${dataOptionsAttr}\n ${scriptClosing}`;
}
const newContent = originalContent.replace(scriptTag, newScriptTag);
return {
success: true,
filePath,
message: "Update React Grab options",
originalContent,
newContent,
};
};
const addOptionsToViteScript = (
originalContent: string,
options: ReactGrabOptions,
filePath: string,
): TransformResult => {
const reactGrabImportWithInitMatch = originalContent.match(
/import\s*\(\s*["']react-grab["']\s*\)(?:\.then\s*\(\s*\(m\)\s*=>\s*m\.init\s*\([^)]*\)\s*\))?/,
);
if (!reactGrabImportWithInitMatch) {
return {
success: false,
filePath,
message: "Could not find React Grab import",
};
}
const optionsJson = formatOptionsAsJson(options);
const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
const newContent = originalContent.replace(
reactGrabImportWithInitMatch[0],
newImport,
);
return {
success: true,
filePath,
message: "Update React Grab options",
originalContent,
newContent,
};
};
const addOptionsToWebpackImport = (
originalContent: string,
options: ReactGrabOptions,
filePath: string,
): TransformResult => {
const reactGrabImportWithInitMatch = originalContent.match(
/import\s*\(\s*["']react-grab["']\s*\)(?:\.then\s*\(\s*\(m\)\s*=>\s*m\.init\s*\([^)]*\)\s*\))?/,
);
if (!reactGrabImportWithInitMatch) {
return {
success: false,
filePath,
message: "Could not find React Grab import",
};
}
const optionsJson = formatOptionsAsJson(options);
const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
const newContent = originalContent.replace(
reactGrabImportWithInitMatch[0],
newImport,
);
return {
success: true,
filePath,
message: "Update React Grab options",
originalContent,
newContent,
};
};
const addOptionsToTanStackImport = (
originalContent: string,
options: ReactGrabOptions,
filePath: string,
): TransformResult => {
const reactGrabImportWithInitMatch = originalContent.match(
/(?:void\s+import\s*\(\s*["']react-grab["']\s*\)|import\s*\(\s*["']react-grab\/core["']\s*\)\.then\s*\(\s*\(\s*\{\s*init\s*\}\s*\)\s*=>\s*init\s*\([^)]*\)\s*\))/,
);
if (!reactGrabImportWithInitMatch) {
return {
success: false,
filePath,
message: "Could not find React Grab import",
};
}
const optionsJson = formatOptionsAsJson(options);
const newImport = `import("react-grab/core").then(({ init }) => init(${optionsJson}))`;
const newContent = originalContent.replace(
reactGrabImportWithInitMatch[0],
newImport,
);
return {
success: true,
filePath,
message: "Update React Grab options",
originalContent,
newContent,
};
};
export const previewOptionsTransform = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
options: ReactGrabOptions,
): TransformResult => {
const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
if (!filePath) {
return {
success: false,
filePath: "",
message: "Could not find file containing React Grab configuration",
};
}
const originalContent = readFileSync(filePath, "utf-8");
if (!hasReactGrabCode(originalContent)) {
return {
success: false,
filePath,
message: "Could not find React Grab code in the file",
};
}
switch (framework) {
case "next":
return addOptionsToNextScript(originalContent, options, filePath);
case "vite":
return addOptionsToViteScript(originalContent, options, filePath);
case "tanstack":
return addOptionsToTanStackImport(originalContent, options, filePath);
case "webpack":
return addOptionsToWebpackImport(originalContent, options, filePath);
default:
return {
success: false,
filePath,
message: `Unknown framework: ${framework}`,
};
}
};
export const applyOptionsTransform = (
result: TransformResult,
): { success: boolean; error?: string } => {
return applyTransform(result);
};
const removeAgentFromNextApp = (
originalContent: string,
agent: string,
filePath: string,
): TransformResult => {
const agentPackage = `@react-grab/${agent}`;
if (!originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is not configured in this file`,
noChanges: true,
};
}
const agentScriptPattern = new RegExp(
`\\s*\\{process\\.env\\.NODE_ENV === "development" && \\(\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>\\s*\\)\\}`,
"gs",
);
const simpleScriptPattern = new RegExp(
`\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>`,
"gi",
);
let newContent = originalContent.replace(agentScriptPattern, "");
if (newContent === originalContent) {
newContent = originalContent.replace(simpleScriptPattern, "");
}
if (newContent === originalContent) {
return {
success: false,
filePath,
message: `Could not find agent ${agent} script to remove`,
};
}
return {
success: true,
filePath,
message: `Remove ${agent} agent`,
originalContent,
newContent,
};
};
const removeAgentFromVite = (
originalContent: string,
agent: string,
filePath: string,
): TransformResult => {
const agentPackage = `@react-grab/${agent}`;
if (!originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is not configured in this file`,
noChanges: true,
};
}
const agentImportPattern = new RegExp(
`\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
"g",
);
const newContent = originalContent.replace(agentImportPattern, "");
if (newContent === originalContent) {
return {
success: false,
filePath,
message: `Could not find agent ${agent} import to remove`,
};
}
return {
success: true,
filePath,
message: `Remove ${agent} agent`,
originalContent,
newContent,
};
};
const removeAgentFromWebpack = (
originalContent: string,
agent: string,
filePath: string,
): TransformResult => {
const agentPackage = `@react-grab/${agent}`;
if (!originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is not configured in this file`,
noChanges: true,
};
}
const agentImportPattern = new RegExp(
`\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
"g",
);
const newContent = originalContent.replace(agentImportPattern, "");
if (newContent === originalContent) {
return {
success: false,
filePath,
message: `Could not find agent ${agent} import to remove`,
};
}
return {
success: true,
filePath,
message: `Remove ${agent} agent`,
originalContent,
newContent,
};
};
const removeAgentFromTanStack = (
originalContent: string,
agent: string,
filePath: string,
): TransformResult => {
const agentPackage = `@react-grab/${agent}`;
if (!originalContent.includes(agentPackage)) {
return {
success: true,
filePath,
message: `Agent ${agent} is not configured in this file`,
noChanges: true,
};
}
const agentImportPattern = new RegExp(
`\\s*void\\s+import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
"g",
);
const newContent = originalContent.replace(agentImportPattern, "");
if (newContent === originalContent) {
return {
success: false,
filePath,
message: `Could not find agent ${agent} import to remove`,
};
}
return {
success: true,
filePath,
message: `Remove ${agent} agent`,
originalContent,
newContent,
};
};
export const previewAgentRemoval = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
agent: string,
): TransformResult => {
const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
if (!filePath) {
return {
success: true,
filePath: "",
message: "Could not find file containing React Grab configuration",
noChanges: true,
};
}
const originalContent = readFileSync(filePath, "utf-8");
switch (framework) {
case "next":
return removeAgentFromNextApp(originalContent, agent, filePath);
case "vite":
return removeAgentFromVite(originalContent, agent, filePath);
case "tanstack":
return removeAgentFromTanStack(originalContent, agent, filePath);
case "webpack":
return removeAgentFromWebpack(originalContent, agent, filePath);
default:
return {
success: false,
filePath,
message: `Unknown framework: ${framework}`,
};
}
};
export const previewPackageJsonAgentRemoval = (
projectRoot: string,
agent: string,
): PackageJsonTransformResult => {
const packageJsonPath = join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return {
success: true,
filePath: "",
message: "Could not find package.json",
noChanges: true,
};
}
const originalContent = readFileSync(packageJsonPath, "utf-8");
const allPrefixVariants = getAllAgentPrefixVariants(agent);
if (allPrefixVariants.length === 0) {
return {
success: true,
filePath: packageJsonPath,
message: `Unknown agent: ${agent}`,
noChanges: true,
};
}
const hasAnyPrefix = allPrefixVariants.some((prefix) =>
originalContent.includes(prefix),
);
if (!hasAnyPrefix) {
return {
success: true,
filePath: packageJsonPath,
message: `Agent ${agent} dev script is not configured`,
noChanges: true,
};
}
try {
const packageJson = JSON.parse(originalContent);
for (const scriptKey of Object.keys(packageJson.scripts || {})) {
let scriptValue = packageJson.scripts[scriptKey];
if (typeof scriptValue === "string") {
for (const prefix of allPrefixVariants) {
if (scriptValue.includes(prefix)) {
scriptValue = scriptValue
.replace(prefix + " ", "")
.replace(prefix, "");
}
}
packageJson.scripts[scriptKey] = scriptValue;
}
}
const newContent = JSON.stringify(packageJson, null, 2) + "\n";
return {
success: true,
filePath: packageJsonPath,
message: `Remove ${agent} server from dev script`,
originalContent,
newContent,
};
} catch {
return {
success: false,
filePath: packageJsonPath,
message: "Failed to parse package.json",
};
}
};
export const previewCdnTransform = (
projectRoot: string,
framework: Framework,
nextRouterType: NextRouterType,
targetCdnDomain: string,
): TransformResult => {
const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
if (!filePath) {
return {
success: false,
filePath: "",
message: "Could not find React Grab file",
};
}
const originalContent = readFileSync(filePath, "utf-8");
const newContent = originalContent
.replace(
/(https?:)?\/\/[^/\s"']+(?=\/(?:@?react-grab))/g,
`//${targetCdnDomain}`,
)
.replace(
/(https?:)?\/\/[^/\s"']*react-grab[^/\s"']*\.com(?=\/script\.js)/g,
`//${targetCdnDomain}`,
);
if (newContent === originalContent) {
return {
success: true,
filePath,
message: "CDN already set",
noChanges: true,
};
}
return {
success: true,
filePath,
message: "Update CDN",
originalContent,
newContent,
};
};
================================================
FILE: packages/cli/test/configure.test.ts
================================================
import { vi, describe, expect, it, beforeEach } from "vitest";
import {
previewOptionsTransform,
applyOptionsTransform,
type ReactGrabOptions,
} from "../src/utils/transform.js";
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
accessSync: vi.fn(),
constants: { W_OK: 2 },
}));
import { existsSync, readFileSync, writeFileSync, accessSync } from "node:fs";
const mockExistsSync = vi.mocked(existsSync);
const mockReadFileSync = vi.mocked(readFileSync);
const mockWriteFileSync = vi.mocked(writeFileSync);
const mockAccessSync = vi.mocked(accessSync);
beforeEach(() => {
vi.clearAllMocks();
});
describe("previewOptionsTransform - Next.js App Router", () => {
const layoutWithReactGrab = `import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
</head>
<body>{children}</body>
</html>
);
}`;
it("should add activationKey option to existing React Grab script", () => {
mockExistsSync.mockImplementation((path) =>
String(path).endsWith("layout.tsx"),
);
mockReadFileSync.mockReturnValue(layoutWithReactGrab);
const options: ReactGrabOptions = {
activationKey: "Meta+K",
};
const result = previewOptionsTransform("/test", "next", "app", options);
expect(result.success).toBe(true);
expect(result.newContent).toContain("data-options");
expect(result.newContent).toContain("activationKey");
expect(result.newContent).toContain("Meta+K");
});
it("should preserve valid JSX format when adding data-options to self-closing Script", () => {
mockExistsSync.mockImplementation((path) =>
String(path).endsWith("layout.tsx"),
);
mockReadFileSync.mockReturnValue(layoutWithReactGrab);
const options: ReactGrabOptions = {
activationMode: "toggle",
};
const result = previewOptionsTransform("/test", "next", "app", options);
expect(result.success).toBe(true);
expect(result.newContent).not.toMatch(/\/\s*\n\s*data-options/);
expect(result.newContent).toMatch(/data-options.*\n\s*\/>/s);
});
it("should not split self-closing tag when adding data-options", () => {
const layoutWithSelfClosingScript = `import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}`;
mockExistsSync.mockImplementation((path) =>
String(path).endsWith("layout.tsx"),
);
mockReadFileSync.mockReturnValue(layoutWithSelfClosingScript);
const options: ReactGrabOptions = {
activationMode: "toggle",
allowActivationInsideInput: false,
maxContextLines: 3,
};
const result = previewOptionsTransform("/test", "next", "app", options);
expect(result.success).toBe(true);
expect(result.newContent).toContain("data-options={JSON.stringify(");
expect(result.newContent).toContain('activationMode: "toggle"');
expect(result.newContent).toContain("allowActivationInsideInput: false");
expect(result.newContent).toContain("maxContextLines: 3");
expect(result.newContent).toContain("/>");
expect(result.newContent).not.toMatch(/\}\)\s*\n\s*\n\s*\/>/);
expect(result.newContent).not.toMatch(
/strategy="beforeInteractive"\s*\/\s*\n/,
);
});
it("should not add extra blank line before closing tag", () => {
const layoutWithScript = `import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{process.env.NODE_ENV === "development" && (
<Script
src="//unpkg.com/react-grab/dist/index.global.js"
crossOrigin="anonymous"
strategy="beforeInteractive"
/>
)}
</head>
<body>{children}</body>
</html>
);
}`;
mockExistsSync.mockImplementation((path) =>
String(path).endsWith("layout.tsx"),
);
mockReadFileSync.mockReturnValue(layoutWithScript);
const options: ReactGrabOptions = {
activationKey: "Meta+K",
};
const result = previewOptionsTransform("/test", "next", "app", options);
expect(result.success).toBe(true);
expect(result.newContent).toContain("data-options");
expect(result.newContent).not.toMatch(/\}\)\n\s*\n\s*\/>/);
});
it("should add multiple options to React Grab scri
gitextract_xtlqk229/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .github/ │ └── workflows/ │ ├── code-quality.yml │ ├── publish-any-commit.yml │ ├── pullfrog.yml │ ├── test-build.yml │ ├── test-cli.yml │ └── test-e2e.yml ├── .gitignore ├── .oxfmtrc.json ├── AGENTS.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── packages/ │ ├── cli/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── commands/ │ │ │ │ ├── add.ts │ │ │ │ ├── configure.ts │ │ │ │ ├── init.ts │ │ │ │ └── remove.ts │ │ │ └── utils/ │ │ │ ├── cli-helpers.ts │ │ │ ├── constants.ts │ │ │ ├── detect.ts │ │ │ ├── diff.ts │ │ │ ├── handle-error.ts │ │ │ ├── highlighter.ts │ │ │ ├── install-mcp.ts │ │ │ ├── install.ts │ │ │ ├── is-non-interactive.ts │ │ │ ├── logger.ts │ │ │ ├── prompts.ts │ │ │ ├── spinner.ts │ │ │ ├── templates.ts │ │ │ └── transform.ts │ │ ├── test/ │ │ │ ├── configure.test.ts │ │ │ ├── detect.test.ts │ │ │ ├── diff.test.ts │ │ │ ├── install-mcp.test.ts │ │ │ ├── install.test.ts │ │ │ ├── templates.test.ts │ │ │ └── transform.test.ts │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── design-system/ │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── e2e-playground/ │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ └── main.tsx │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── grab/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin/ │ │ │ └── cli.js │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── build.js │ │ └── tsconfig.json │ ├── gym/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── api/ │ │ │ │ └── provider/ │ │ │ │ └── [name]/ │ │ │ │ └── route.ts │ │ │ ├── dashboard/ │ │ │ │ ├── data.json │ │ │ │ └── page.tsx │ │ │ ├── freeze-demo/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── login/ │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── playground/ │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── agent-playground.tsx │ │ │ ├── app-sidebar.tsx │ │ │ ├── chart-area-interactive.tsx │ │ │ ├── counter.tsx │ │ │ ├── data-table.tsx │ │ │ ├── login-form.tsx │ │ │ ├── nav-user.tsx │ │ │ ├── search-form.tsx │ │ │ ├── section-cards.tsx │ │ │ ├── sheet-demo.tsx │ │ │ ├── site-header.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-toggle.tsx │ │ │ ├── ui/ │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sidebar.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── version-switcher.tsx │ │ ├── components.json │ │ ├── hooks/ │ │ │ └── use-mobile.ts │ │ ├── instrumentation-client.ts │ │ ├── lib/ │ │ │ └── utils.ts │ │ ├── next-env.d.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── scripts/ │ │ │ └── start-all-servers.js │ │ └── tsconfig.json │ ├── mcp/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── constants.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-amp/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-claude-code/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-codex/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-copilot/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-cursor/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-droid/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-gemini/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── provider-opencode/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── cli.ts │ │ │ ├── client.ts │ │ │ ├── constants.ts │ │ │ ├── handler.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── react-grab/ │ │ ├── .oxlintrc.json │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── bin/ │ │ │ └── cli.js │ │ ├── e2e/ │ │ │ ├── activation-key-config.spec.ts │ │ │ ├── activation.spec.ts │ │ │ ├── agent-integration.spec.ts │ │ │ ├── agent-resume-race.spec.ts │ │ │ ├── api-methods.spec.ts │ │ │ ├── clear-history-prompt.spec.ts │ │ │ ├── context-menu.spec.ts │ │ │ ├── copy-feedback.spec.ts │ │ │ ├── copy-styles.spec.ts │ │ │ ├── disabled-elements.spec.ts │ │ │ ├── drag-selection.spec.ts │ │ │ ├── edge-cases.spec.ts │ │ │ ├── element-context.spec.ts │ │ │ ├── event-callbacks.spec.ts │ │ │ ├── fixtures.ts │ │ │ ├── focus-trap.spec.ts │ │ │ ├── freeze-animations.spec.ts │ │ │ ├── freeze-updates.spec.ts │ │ │ ├── history-items.spec.ts │ │ │ ├── history-reacquire.spec.ts │ │ │ ├── hold-activation.spec.ts │ │ │ ├── input-mode.spec.ts │ │ │ ├── keyboard-navigation.spec.ts │ │ │ ├── keyboard-shortcuts.spec.ts │ │ │ ├── open-file.spec.ts │ │ │ ├── overlay-filtering.spec.ts │ │ │ ├── prompt-mode.spec.ts │ │ │ ├── selection.spec.ts │ │ │ ├── ssr.spec.ts │ │ │ ├── theme-customization.spec.ts │ │ │ ├── toggle-position-stability.spec.ts │ │ │ ├── toolbar-menu.spec.ts │ │ │ ├── toolbar-selection-hover.spec.ts │ │ │ ├── toolbar.spec.ts │ │ │ ├── touch-mode.spec.ts │ │ │ ├── viewport.spec.ts │ │ │ └── visual-feedback.spec.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── scripts/ │ │ │ ├── css-rem-to-px.mjs │ │ │ └── postinstall.cjs │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── clear-history-prompt.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── history-dropdown.tsx │ │ │ │ ├── icons/ │ │ │ │ │ ├── icon-check.tsx │ │ │ │ │ ├── icon-chevron.tsx │ │ │ │ │ ├── icon-clock.tsx │ │ │ │ │ ├── icon-copy.tsx │ │ │ │ │ ├── icon-ellipsis.tsx │ │ │ │ │ ├── icon-loader.tsx │ │ │ │ │ ├── icon-open.tsx │ │ │ │ │ ├── icon-reply.tsx │ │ │ │ │ ├── icon-retry.tsx │ │ │ │ │ ├── icon-return.tsx │ │ │ │ │ ├── icon-select.tsx │ │ │ │ │ ├── icon-submit.tsx │ │ │ │ │ └── icon-trash.tsx │ │ │ │ ├── kbd.tsx │ │ │ │ ├── overlay-canvas.tsx │ │ │ │ ├── renderer.tsx │ │ │ │ ├── selection-label/ │ │ │ │ │ ├── arrow-navigation-menu.tsx │ │ │ │ │ ├── arrow.tsx │ │ │ │ │ ├── bottom-section.tsx │ │ │ │ │ ├── completion-view.tsx │ │ │ │ │ ├── discard-prompt.tsx │ │ │ │ │ ├── error-view.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── tag-badge.tsx │ │ │ │ ├── toolbar/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── state.ts │ │ │ │ │ ├── toolbar-content.tsx │ │ │ │ │ └── toolbar-menu.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── constants.ts │ │ │ ├── core/ │ │ │ │ ├── agent/ │ │ │ │ │ ├── manager.ts │ │ │ │ │ └── session.ts │ │ │ │ ├── arrow-navigation.ts │ │ │ │ ├── auto-scroll.ts │ │ │ │ ├── context.ts │ │ │ │ ├── copy.ts │ │ │ │ ├── events.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── keyboard-handlers.ts │ │ │ │ ├── log-intro.ts │ │ │ │ ├── logo-svg.ts │ │ │ │ ├── noop-api.ts │ │ │ │ ├── plugin-registry.ts │ │ │ │ ├── plugins/ │ │ │ │ │ ├── comment.ts │ │ │ │ │ ├── copy-html.ts │ │ │ │ │ ├── copy-styles.ts │ │ │ │ │ ├── copy.ts │ │ │ │ │ ├── create-pending-selection-plugin.ts │ │ │ │ │ └── open.ts │ │ │ │ ├── store.ts │ │ │ │ └── theme.ts │ │ │ ├── index.ts │ │ │ ├── primitives.ts │ │ │ ├── styles.css │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── append-stack-context.ts │ │ │ ├── auto-resize-textarea.ts │ │ │ ├── clamp-to-viewport.ts │ │ │ ├── cn.ts │ │ │ ├── combine-bounds.ts │ │ │ ├── confirmation-focus-manager.ts │ │ │ ├── copy-content.ts │ │ │ ├── create-anchored-dropdown.ts │ │ │ ├── create-bounds-from-drag-rect.ts │ │ │ ├── create-element-bounds.ts │ │ │ ├── create-element-selector.ts │ │ │ ├── create-menu-highlight.ts │ │ │ ├── create-style-element.ts │ │ │ ├── create-toolbar-drag.ts │ │ │ ├── extract-element-css.ts │ │ │ ├── format-relative-time.ts │ │ │ ├── format-shortcut.ts │ │ │ ├── freeze-animations.ts │ │ │ ├── freeze-gsap.ts │ │ │ ├── freeze-pseudo-states.ts │ │ │ ├── freeze-updates.ts │ │ │ ├── generate-id.ts │ │ │ ├── generate-snippet.ts │ │ │ ├── get-anchored-dropdown-position.ts │ │ │ ├── get-arrow-size.ts │ │ │ ├── get-bounds-center.ts │ │ │ ├── get-element-at-position.ts │ │ │ ├── get-element-center.ts │ │ │ ├── get-elements-in-drag.ts │ │ │ ├── get-next-base-path.ts │ │ │ ├── get-script-options.ts │ │ │ ├── get-tag-display.ts │ │ │ ├── get-tag-name.ts │ │ │ ├── get-visible-bounds-center.ts │ │ │ ├── get-visual-viewport.ts │ │ │ ├── history-storage.ts │ │ │ ├── invalidate-interaction-caches.ts │ │ │ ├── is-c-like-key.ts │ │ │ ├── is-element-connected.ts │ │ │ ├── is-element-visible.ts │ │ │ ├── is-enter-code.ts │ │ │ ├── is-event-from-overlay.ts │ │ │ ├── is-extension-context.ts │ │ │ ├── is-keyboard-event-triggered-by-input.ts │ │ │ ├── is-mac.ts │ │ │ ├── is-root-element.ts │ │ │ ├── is-target-key-combination.ts │ │ │ ├── is-valid-grabbable-element.ts │ │ │ ├── join-snippets.ts │ │ │ ├── key-matches-code.ts │ │ │ ├── lerp.ts │ │ │ ├── log-recoverable-error.ts │ │ │ ├── mount-root.ts │ │ │ ├── native-raf.ts │ │ │ ├── normalize-error.ts │ │ │ ├── on-idle.ts │ │ │ ├── open-file.ts │ │ │ ├── overlay-color.ts │ │ │ ├── parse-activation-key.ts │ │ │ ├── recalculate-session-position.ts │ │ │ ├── register-overlay-dismiss.ts │ │ │ ├── resolve-action-enabled.ts │ │ │ ├── safe-polygon.ts │ │ │ ├── strip-translate-from-transform.ts │ │ │ ├── supports-display-p3.ts │ │ │ ├── suppress-menu-event.ts │ │ │ ├── toolbar-layout.ts │ │ │ ├── toolbar-position.ts │ │ │ └── truncate-string.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── relay/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── client.ts │ │ │ ├── connection.ts │ │ │ ├── index.ts │ │ │ ├── protocol.ts │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── shadcn-registry/ │ │ ├── package.json │ │ ├── r/ │ │ │ └── react-grab.json │ │ ├── registry/ │ │ │ └── react-grab.tsx │ │ ├── registry.json │ │ └── scripts/ │ │ └── build.js │ ├── utils/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── web-extension/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── package.sh │ │ ├── src/ │ │ │ ├── background/ │ │ │ │ └── service-worker.ts │ │ │ ├── constants.ts │ │ │ ├── content/ │ │ │ │ ├── bridge.ts │ │ │ │ └── react-grab.ts │ │ │ └── manifest.json │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── website/ │ ├── .gitignore │ ├── .oxlintrc.json │ ├── AGENTS.md │ ├── app/ │ │ ├── api/ │ │ │ ├── og/ │ │ │ │ └── route.tsx │ │ │ ├── report-cli/ │ │ │ │ └── route.ts │ │ │ └── version/ │ │ │ └── route.ts │ │ ├── blog/ │ │ │ ├── 1-0/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── agent/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── bets/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── intro/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── changelog/ │ │ │ └── page.tsx │ │ ├── design-system/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── open-file/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── privacy/ │ │ │ └── page.tsx │ │ ├── robots.ts │ │ └── sitemap.ts │ ├── components/ │ │ ├── benchmark-tooltip.tsx │ │ ├── benchmarks/ │ │ │ ├── benchmark-charts.tsx │ │ │ ├── benchmark-detailed-table.tsx │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── blocks/ │ │ │ ├── grep-search-group.tsx │ │ │ ├── grep-tool-call-block.tsx │ │ │ ├── message-block.tsx │ │ │ ├── read-tool-call-block.tsx │ │ │ ├── streaming-text.tsx │ │ │ └── thought-block.tsx │ │ ├── blog-article-layout.tsx │ │ ├── demo-footer.tsx │ │ ├── github-button.tsx │ │ ├── grab-element-button.tsx │ │ ├── homepage-demo.tsx │ │ ├── hotkey-context.tsx │ │ ├── icons/ │ │ │ ├── icon-claude.tsx │ │ │ ├── icon-codex.tsx │ │ │ ├── icon-copilot.tsx │ │ │ ├── icon-cursor.tsx │ │ │ ├── icon-droid.tsx │ │ │ ├── icon-github.tsx │ │ │ ├── icon-nextjs.tsx │ │ │ ├── icon-opencode.tsx │ │ │ ├── icon-tanstack.tsx │ │ │ ├── icon-vite.tsx │ │ │ ├── icon-vscode.tsx │ │ │ ├── icon-webstorm.tsx │ │ │ └── icon-zed.tsx │ │ ├── install-tabs.tsx │ │ ├── mobile-demo-animation.tsx │ │ ├── react-grab-logo.tsx │ │ ├── table-of-contents.tsx │ │ ├── ui/ │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── data-table-card.tsx │ │ │ └── scrollable.tsx │ │ ├── user-message.tsx │ │ └── view-docs-button.tsx │ ├── components.json │ ├── constants.ts │ ├── hooks/ │ │ └── use-stream.ts │ ├── instrumentation-client.ts │ ├── lib/ │ │ ├── api-helpers.ts │ │ └── shiki.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public/ │ │ ├── agent.webm │ │ ├── demo.webm │ │ ├── install.md │ │ ├── llms.txt │ │ ├── r/ │ │ │ ├── index.json │ │ │ └── react-grab.json │ │ ├── results.json │ │ └── test-cases.json │ ├── tsconfig.json │ └── utils/ │ ├── cn.ts │ ├── detect-mobile.ts │ ├── get-key-from-code.ts │ ├── hotkey-to-string.ts │ └── parse-changelog.ts ├── pnpm-workspace.yaml ├── turbo.json └── vercel.json
SYMBOL INDEX (893 symbols across 231 files)
FILE: packages/cli/src/cli.ts
constant VERSION (line 7) | const VERSION = process.env.VERSION ?? "0.0.1";
constant VERSION_API_URL (line 8) | const VERSION_API_URL = "https://www.react-grab.com/api/version";
FILE: packages/cli/src/commands/add.ts
constant VERSION (line 38) | const VERSION = process.env.VERSION ?? "0.0.1";
FILE: packages/cli/src/commands/configure.ts
constant VERSION (line 23) | const VERSION = process.env.VERSION ?? "0.0.1";
type ConfigOption (line 25) | interface ConfigOption {
constant META_LABEL (line 32) | const META_LABEL = isMac ? "Cmd" : "Win";
constant ALT_LABEL (line 33) | const ALT_LABEL = isMac ? "Option" : "Alt";
constant MODIFIER_ALIASES (line 35) | const MODIFIER_ALIASES: Record<string, string> = {
constant MODIFIERS (line 49) | const MODIFIERS = ["meta", "ctrl", "shift", "alt"] as const;
constant BASE_KEYS (line 51) | const BASE_KEYS: Array<{ key: string; aliases: string[] }> = [
type KeyCombo (line 92) | interface KeyCombo {
type KeyChoice (line 100) | interface KeyChoice {
constant POPULAR_KEYS (line 142) | const POPULAR_KEYS = ["g", "k", "e", "d", "b", " ", "Escape", "Enter"];
constant CONFIG_OPTIONS (line 230) | const CONFIG_OPTIONS: ConfigOption[] = [
FILE: packages/cli/src/commands/init.ts
constant VERSION (line 49) | const VERSION = process.env.VERSION ?? "0.0.1";
constant REPORT_URL (line 50) | const REPORT_URL = "https://react-grab.com/api/report-cli";
constant DOCS_URL (line 51) | const DOCS_URL = "https://github.com/aidenybai/react-grab";
type ReportConfig (line 53) | interface ReportConfig {
constant FRAMEWORK_NAMES (line 79) | const FRAMEWORK_NAMES: Record<Framework, string> = {
constant PACKAGE_MANAGER_NAMES (line 87) | const PACKAGE_MANAGER_NAMES: Record<PackageManager, string> = {
constant UNSUPPORTED_FRAMEWORK_NAMES (line 94) | const UNSUPPORTED_FRAMEWORK_NAMES: Record<
FILE: packages/cli/src/commands/remove.ts
constant VERSION (line 20) | const VERSION = process.env.VERSION ?? "0.0.1";
FILE: packages/cli/src/utils/constants.ts
constant MAX_SUGGESTIONS_COUNT (line 1) | const MAX_SUGGESTIONS_COUNT = 30;
constant MAX_KEY_HOLD_DURATION_MS (line 2) | const MAX_KEY_HOLD_DURATION_MS = 2000;
constant MAX_CONTEXT_LINES (line 3) | const MAX_CONTEXT_LINES = 50;
FILE: packages/cli/src/utils/detect.ts
type PackageManager (line 7) | type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
type Framework (line 8) | type Framework = "next" | "vite" | "tanstack" | "webpack" | "unknown";
type NextRouterType (line 9) | type NextRouterType = "app" | "pages" | "unknown";
type UnsupportedFramework (line 10) | type UnsupportedFramework =
type ProjectInfo (line 17) | interface ProjectInfo {
constant VALID_PACKAGE_MANAGERS (line 28) | const VALID_PACKAGE_MANAGERS: ReadonlySet<string> = new Set([
type WorkspaceProject (line 126) | interface WorkspaceProject {
constant ALWAYS_IGNORED_DIRECTORIES (line 261) | const ALWAYS_IGNORED_DIRECTORIES = [
constant MAX_SCAN_DEPTH (line 328) | const MAX_SCAN_DEPTH = 2;
constant AGENT_PACKAGES (line 421) | const AGENT_PACKAGES = [
type AgentCLI (line 493) | type AgentCLI =
constant AGENT_CLI_COMMANDS (line 503) | const AGENT_CLI_COMMANDS: AgentCLI[] = [
FILE: packages/cli/src/utils/diff.ts
type DiffLine (line 1) | interface DiffLine {
constant RED (line 7) | const RED = "\x1b[31m";
constant GREEN (line 8) | const GREEN = "\x1b[32m";
constant GRAY (line 9) | const GRAY = "\x1b[90m";
constant RESET (line 10) | const RESET = "\x1b[0m";
constant BOLD (line 11) | const BOLD = "\x1b[1m";
FILE: packages/cli/src/utils/install-mcp.ts
constant SERVER_NAME (line 12) | const SERVER_NAME = "react-grab-mcp";
constant PACKAGE_NAME (line 13) | const PACKAGE_NAME = "@react-grab/mcp";
type ClientDefinition (line 15) | interface ClientDefinition {
type InstallResult (line 23) | interface InstallResult {
constant JSONC_FORMAT_OPTIONS (line 150) | const JSONC_FORMAT_OPTIONS: jsonc.FormattingOptions = {
FILE: packages/cli/src/utils/install.ts
constant INSTALL_COMMANDS (line 5) | const INSTALL_COMMANDS: Record<PackageManager, string> = {
constant UNINSTALL_COMMANDS (line 12) | const UNINSTALL_COMMANDS: Record<PackageManager, string> = {
FILE: packages/cli/src/utils/is-non-interactive.ts
constant AGENT_ENVIRONMENT_VARIABLES (line 1) | const AGENT_ENVIRONMENT_VARIABLES = [
FILE: packages/cli/src/utils/logger.ts
method error (line 4) | error(...args: unknown[]) {
method warn (line 7) | warn(...args: unknown[]) {
method info (line 10) | info(...args: unknown[]) {
method success (line 13) | success(...args: unknown[]) {
method dim (line 16) | dim(...args: unknown[]) {
method log (line 19) | log(...args: unknown[]) {
method break (line 22) | break() {
FILE: packages/cli/src/utils/spinner.ts
type SpinnerOptions (line 3) | interface SpinnerOptions {
FILE: packages/cli/src/utils/templates.ts
constant AGENTS (line 1) | const AGENTS = [
type Agent (line 12) | type Agent = (typeof AGENTS)[number];
type AgentIntegration (line 14) | type AgentIntegration = Agent | "mcp" | "none";
constant AGENT_NAMES (line 16) | const AGENT_NAMES: Record<Agent, string> = {
constant NEXT_APP_ROUTER_SCRIPT (line 35) | const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" ...
constant NEXT_PAGES_ROUTER_SCRIPT (line 63) | const NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development...
constant VITE_IMPORT (line 91) | const VITE_IMPORT = `if (import.meta.env.DEV) {
constant WEBPACK_IMPORT (line 104) | const WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
constant TANSTACK_EFFECT (line 117) | const TANSTACK_EFFECT = `useEffect(() => {
constant SCRIPT_IMPORT (line 134) | const SCRIPT_IMPORT = 'import Script from "next/script";';
FILE: packages/cli/src/utils/transform.ts
type TransformResult (line 20) | interface TransformResult {
type ReactGrabOptions (line 29) | interface ReactGrabOptions {
type PackageJsonTransformResult (line 37) | interface PackageJsonTransformResult {
constant AGENT_PACKAGES (line 851) | const AGENT_PACKAGES: Record<string, string> = {
FILE: packages/design-system/src/index.tsx
type ComponentType (line 15) | type ComponentType = "label" | "context-menu" | "toolbar" | "history-dro...
type DesignSystemStateProps (line 17) | interface DesignSystemStateProps {
type AnimationFrame (line 52) | interface AnimationFrame {
type DesignSystemState (line 57) | interface DesignSystemState {
constant DESIGN_SYSTEM_STATES (line 66) | const DESIGN_SYSTEM_STATES: DesignSystemState[] = [
constant CELL_SIZE_PX (line 2080) | const CELL_SIZE_PX = 300;
constant TARGET_HEIGHT_PX (line 2081) | const TARGET_HEIGHT_PX = 48;
constant GAP_PX (line 2082) | const GAP_PX = 16;
constant CARD_BORDER_RADIUS_PX (line 2084) | const CARD_BORDER_RADIUS_PX = 8;
constant CARD_HEADER_PADDING (line 2085) | const CARD_HEADER_PADDING = "12px 14px";
constant CARD_CONTENT_PADDING_PX (line 2086) | const CARD_CONTENT_PADDING_PX = 16;
constant CARD_TITLE_FONT_SIZE_PX (line 2087) | const CARD_TITLE_FONT_SIZE_PX = 13;
constant CARD_DESCRIPTION_FONT_SIZE_PX (line 2088) | const CARD_DESCRIPTION_FONT_SIZE_PX = 11;
constant CARD_TITLE_GAP_PX (line 2089) | const CARD_TITLE_GAP_PX = 2;
constant REFRESH_BUTTON_SIZE_PX (line 2091) | const REFRESH_BUTTON_SIZE_PX = 20;
constant REFRESH_BUTTON_BORDER_RADIUS_PX (line 2092) | const REFRESH_BUTTON_BORDER_RADIUS_PX = 4;
constant HEADER_PADDING (line 2094) | const HEADER_PADDING = "16px 24px";
constant HEADER_TITLE_FONT_SIZE_PX (line 2095) | const HEADER_TITLE_FONT_SIZE_PX = 14;
constant HEADER_BUTTONS_GAP_PX (line 2096) | const HEADER_BUTTONS_GAP_PX = 8;
constant TOGGLE_BUTTON_PADDING (line 2098) | const TOGGLE_BUTTON_PADDING = "5px 10px";
constant TOGGLE_BUTTON_GAP_PX (line 2099) | const TOGGLE_BUTTON_GAP_PX = 6;
constant TOGGLE_BUTTON_BORDER_RADIUS_PX (line 2100) | const TOGGLE_BUTTON_BORDER_RADIUS_PX = 6;
constant TOGGLE_BUTTON_FONT_SIZE_PX (line 2101) | const TOGGLE_BUTTON_FONT_SIZE_PX = 12;
constant SECTION_TITLE_FONT_SIZE_PX (line 2103) | const SECTION_TITLE_FONT_SIZE_PX = 11;
constant SECTION_TITLE_MARGIN_BOTTOM_PX (line 2104) | const SECTION_TITLE_MARGIN_BOTTOM_PX = 12;
constant FPS_METER_POSITION_PX (line 2106) | const FPS_METER_POSITION_PX = 16;
constant FPS_METER_PADDING (line 2107) | const FPS_METER_PADDING = "6px 10px";
constant FPS_METER_BORDER_RADIUS_PX (line 2108) | const FPS_METER_BORDER_RADIUS_PX = 6;
constant FPS_METER_FONT_SIZE_PX (line 2109) | const FPS_METER_FONT_SIZE_PX = 12;
constant TARGET_BORDER_RADIUS_PX (line 2111) | const TARGET_BORDER_RADIUS_PX = 6;
constant TARGET_FONT_SIZE_PX (line 2112) | const TARGET_FONT_SIZE_PX = 12;
constant TRANSITION_DURATION (line 2114) | const TRANSITION_DURATION = "0.15s ease";
constant STORAGE_KEY_THEME (line 2116) | const STORAGE_KEY_THEME = "react-grab-design-system-theme";
constant STORAGE_KEY_STARRED (line 2117) | const STORAGE_KEY_STARRED = "react-grab-design-system-starred";
type ThemeColors (line 2183) | interface ThemeColors {
constant DARK_THEME (line 2200) | const DARK_THEME: ThemeColors = {
constant LIGHT_THEME (line 2217) | const LIGHT_THEME: ThemeColors = {
type StateCardProps (line 2300) | interface StateCardProps {
type FpsMeterProps (line 2700) | interface FpsMeterProps {
type DesignSystemPreviewOptions (line 3305) | interface DesignSystemPreviewOptions {
FILE: packages/e2e-playground/src/App.tsx
type Todo (line 3) | interface Todo {
function App (line 684) | function App() {
FILE: packages/e2e-playground/src/main.tsx
type Window (line 8) | interface Window {
FILE: packages/gym/app/api/provider/[name]/route.ts
constant PROVIDER_MAP (line 5) | const PROVIDER_MAP: Record<string, string> = {
FILE: packages/gym/app/dashboard/page.tsx
function Page (line 12) | function Page() {
FILE: packages/gym/app/freeze-demo/layout.tsx
function FreezeDemoLayout (line 1) | function FreezeDemoLayout({
FILE: packages/gym/app/freeze-demo/page.tsx
constant TIMER_INTERVAL_MS (line 5) | const TIMER_INTERVAL_MS = 10;
constant VELOCITY_PX (line 6) | const VELOCITY_PX = 3;
function FreezeDemoPage (line 95) | function FreezeDemoPage() {
FILE: packages/gym/app/layout.tsx
function RootLayout (line 22) | function RootLayout({
FILE: packages/gym/app/login/page.tsx
function Page (line 3) | function Page() {
FILE: packages/gym/app/page.tsx
function Home (line 3) | function Home() {
FILE: packages/gym/app/playground/page.tsx
function PlaygroundPage (line 6) | function PlaygroundPage() {
FILE: packages/gym/components/agent-playground.tsx
type RelayClient (line 10) | interface RelayClient {
type RelayMessage (line 18) | interface RelayMessage {
type Window (line 27) | interface Window {
type LogEntry (line 33) | interface LogEntry {
constant LOG_TYPE_STYLES (line 39) | const LOG_TYPE_STYLES: Record<string, { icon: string; color: string }> = {
constant MAX_LOG_ENTRIES (line 49) | const MAX_LOG_ENTRIES = 50;
constant STATUS_TRUNCATE_LENGTH (line 50) | const STATUS_TRUNCATE_LENGTH = 60;
constant RELAY_CHECK_INTERVAL_MS (line 51) | const RELAY_CHECK_INTERVAL_MS = 100;
constant PROVIDER_SCRIPTS (line 53) | const PROVIDER_SCRIPTS: Record<string, string> = {
type ProviderBadgeProps (line 64) | interface ProviderBadgeProps {
type AgentPlaygroundProps (line 82) | interface AgentPlaygroundProps {
FILE: packages/gym/components/app-sidebar.tsx
function AppSidebar (line 102) | function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
FILE: packages/gym/components/chart-area-interactive.tsx
function ChartAreaInteractive (line 140) | function ChartAreaInteractive() {
FILE: packages/gym/components/counter.tsx
constant COUNTER_INTERVAL_MS (line 5) | const COUNTER_INTERVAL_MS = 100;
FILE: packages/gym/components/data-table.tsx
function DragHandle (line 115) | function DragHandle({ id }: { id: number }) {
function DraggableRow (line 309) | function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
function DataTable (line 334) | function DataTable({
function TableCellViewer (line 646) | function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
FILE: packages/gym/components/login-form.tsx
function LoginForm (line 18) | function LoginForm({
FILE: packages/gym/components/nav-user.tsx
function NavUser (line 28) | function NavUser({
FILE: packages/gym/components/search-form.tsx
function SearchForm (line 10) | function SearchForm({ ...props }: React.ComponentProps<"form">) {
FILE: packages/gym/components/section-cards.tsx
function SectionCards (line 13) | function SectionCards() {
FILE: packages/gym/components/ui/avatar.tsx
function Avatar (line 8) | function Avatar({
function AvatarImage (line 24) | function AvatarImage({
function AvatarFallback (line 37) | function AvatarFallback({
FILE: packages/gym/components/ui/badge.tsx
function Badge (line 28) | function Badge({
FILE: packages/gym/components/ui/button.tsx
function Button (line 39) | function Button({
FILE: packages/gym/components/ui/card.tsx
function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
FILE: packages/gym/components/ui/chart.tsx
constant THEMES (line 9) | const THEMES = { light: "", dark: ".dark" } as const;
type ChartConfig (line 11) | type ChartConfig = {
type ChartContextProps (line 21) | type ChartContextProps = {
function useChart (line 27) | function useChart() {
function ChartContainer (line 37) | function ChartContainer({
function ChartTooltipContent (line 107) | function ChartTooltipContent({
function ChartLegendContent (line 255) | function ChartLegendContent({
function getPayloadConfigFromPayload (line 312) | function getPayloadConfigFromPayload(
FILE: packages/gym/components/ui/checkbox.tsx
function Checkbox (line 9) | function Checkbox({
FILE: packages/gym/components/ui/drawer.tsx
function Drawer (line 8) | function Drawer({
function DrawerTrigger (line 14) | function DrawerTrigger({
function DrawerPortal (line 20) | function DrawerPortal({
function DrawerClose (line 26) | function DrawerClose({
function DrawerOverlay (line 32) | function DrawerOverlay({
function DrawerContent (line 48) | function DrawerContent({
function DrawerHeader (line 75) | function DrawerHeader({ className, ...props }: React.ComponentProps<"div...
function DrawerFooter (line 88) | function DrawerFooter({ className, ...props }: React.ComponentProps<"div...
function DrawerTitle (line 98) | function DrawerTitle({
function DrawerDescription (line 111) | function DrawerDescription({
FILE: packages/gym/components/ui/dropdown-menu.tsx
function DropdownMenu (line 9) | function DropdownMenu({
function DropdownMenuPortal (line 15) | function DropdownMenuPortal({
function DropdownMenuTrigger (line 23) | function DropdownMenuTrigger({
function DropdownMenuContent (line 34) | function DropdownMenuContent({
function DropdownMenuGroup (line 54) | function DropdownMenuGroup({
function DropdownMenuItem (line 62) | function DropdownMenuItem({
function DropdownMenuCheckboxItem (line 85) | function DropdownMenuCheckboxItem({
function DropdownMenuRadioGroup (line 111) | function DropdownMenuRadioGroup({
function DropdownMenuRadioItem (line 122) | function DropdownMenuRadioItem({
function DropdownMenuLabel (line 146) | function DropdownMenuLabel({
function DropdownMenuSeparator (line 166) | function DropdownMenuSeparator({
function DropdownMenuShortcut (line 179) | function DropdownMenuShortcut({
function DropdownMenuSub (line 195) | function DropdownMenuSub({
function DropdownMenuSubTrigger (line 201) | function DropdownMenuSubTrigger({
function DropdownMenuSubContent (line 225) | function DropdownMenuSubContent({
FILE: packages/gym/components/ui/field.tsx
function FieldSet (line 10) | function FieldSet({ className, ...props }: React.ComponentProps<"fieldse...
function FieldLegend (line 24) | function FieldLegend({
function FieldGroup (line 44) | function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
function Field (line 81) | function Field({
function FieldContent (line 97) | function FieldContent({ className, ...props }: React.ComponentProps<"div...
function FieldLabel (line 110) | function FieldLabel({
function FieldTitle (line 128) | function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
function FieldDescription (line 141) | function FieldDescription({ className, ...props }: React.ComponentProps<...
function FieldSeparator (line 156) | function FieldSeparator({
function FieldError (line 186) | function FieldError({
FILE: packages/gym/components/ui/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...
FILE: packages/gym/components/ui/label.tsx
function Label (line 8) | function Label({
FILE: packages/gym/components/ui/select.tsx
function Select (line 9) | function Select({
function SelectGroup (line 15) | function SelectGroup({
function SelectValue (line 21) | function SelectValue({
function SelectTrigger (line 27) | function SelectTrigger({
function SelectContent (line 53) | function SelectContent({
function SelectLabel (line 90) | function SelectLabel({
function SelectItem (line 103) | function SelectItem({
function SelectSeparator (line 130) | function SelectSeparator({
function SelectScrollUpButton (line 143) | function SelectScrollUpButton({
function SelectScrollDownButton (line 161) | function SelectScrollDownButton({
FILE: packages/gym/components/ui/separator.tsx
function Separator (line 8) | function Separator({
FILE: packages/gym/components/ui/sheet.tsx
function Sheet (line 9) | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive....
function SheetTrigger (line 13) | function SheetTrigger({
function SheetClose (line 19) | function SheetClose({
function SheetPortal (line 25) | function SheetPortal({
function SheetOverlay (line 31) | function SheetOverlay({
function SheetContent (line 47) | function SheetContent({
function SheetHeader (line 84) | function SheetHeader({ className, ...props }: React.ComponentProps<"div"...
function SheetFooter (line 94) | function SheetFooter({ className, ...props }: React.ComponentProps<"div"...
function SheetTitle (line 104) | function SheetTitle({
function SheetDescription (line 117) | function SheetDescription({
FILE: packages/gym/components/ui/sidebar.tsx
constant SIDEBAR_COOKIE_NAME (line 28) | const SIDEBAR_COOKIE_NAME = "sidebar_state";
constant SIDEBAR_COOKIE_MAX_AGE (line 29) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
constant SIDEBAR_WIDTH (line 30) | const SIDEBAR_WIDTH = "16rem";
constant SIDEBAR_WIDTH_MOBILE (line 31) | const SIDEBAR_WIDTH_MOBILE = "18rem";
constant SIDEBAR_WIDTH_ICON (line 32) | const SIDEBAR_WIDTH_ICON = "3rem";
constant SIDEBAR_KEYBOARD_SHORTCUT (line 33) | const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps (line 35) | type SidebarContextProps = {
function useSidebar (line 47) | function useSidebar() {
function SidebarProvider (line 56) | function SidebarProvider({
function Sidebar (line 154) | function Sidebar({
function SidebarTrigger (line 256) | function SidebarTrigger({
function SidebarRail (line 282) | function SidebarRail({ className, ...props }: React.ComponentProps<"butt...
function SidebarInset (line 307) | function SidebarInset({ className, ...props }: React.ComponentProps<"mai...
function SidebarInput (line 321) | function SidebarInput({
function SidebarHeader (line 335) | function SidebarHeader({ className, ...props }: React.ComponentProps<"di...
function SidebarFooter (line 346) | function SidebarFooter({ className, ...props }: React.ComponentProps<"di...
function SidebarSeparator (line 357) | function SidebarSeparator({
function SidebarContent (line 371) | function SidebarContent({ className, ...props }: React.ComponentProps<"d...
function SidebarGroup (line 385) | function SidebarGroup({ className, ...props }: React.ComponentProps<"div...
function SidebarGroupLabel (line 396) | function SidebarGroupLabel({
function SidebarGroupAction (line 417) | function SidebarGroupAction({
function SidebarGroupContent (line 440) | function SidebarGroupContent({
function SidebarMenu (line 454) | function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenuItem (line 465) | function SidebarMenuItem({ className, ...props }: React.ComponentProps<"...
function SidebarMenuButton (line 498) | function SidebarMenuButton({
function SidebarMenuAction (line 548) | function SidebarMenuAction({
function SidebarMenuBadge (line 580) | function SidebarMenuBadge({
function SidebarMenuSkeleton (line 602) | function SidebarMenuSkeleton({
function SidebarMenuSub (line 643) | function SidebarMenuSub({ className, ...props }: React.ComponentProps<"u...
function SidebarMenuSubItem (line 658) | function SidebarMenuSubItem({
function SidebarMenuSubButton (line 672) | function SidebarMenuSubButton({
FILE: packages/gym/components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
FILE: packages/gym/components/ui/table.tsx
function Table (line 7) | function Table({ className, ...props }: React.ComponentProps<"table">) {
function TableHeader (line 22) | function TableHeader({ className, ...props }: React.ComponentProps<"thea...
function TableBody (line 32) | function TableBody({ className, ...props }: React.ComponentProps<"tbody"...
function TableFooter (line 42) | function TableFooter({ className, ...props }: React.ComponentProps<"tfoo...
function TableRow (line 55) | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
function TableHead (line 68) | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
function TableCell (line 81) | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
function TableCaption (line 94) | function TableCaption({
FILE: packages/gym/components/ui/tabs.tsx
function Tabs (line 8) | function Tabs({
function TabsList (line 21) | function TabsList({
function TabsTrigger (line 37) | function TabsTrigger({
function TabsContent (line 53) | function TabsContent({
FILE: packages/gym/components/ui/toggle-group.tsx
function ToggleGroup (line 20) | function ToggleGroup({
function ToggleGroupItem (line 51) | function ToggleGroupItem({
FILE: packages/gym/components/ui/toggle.tsx
function Toggle (line 31) | function Toggle({
FILE: packages/gym/components/ui/tooltip.tsx
function TooltipProvider (line 8) | function TooltipProvider({
function Tooltip (line 21) | function Tooltip({
function TooltipTrigger (line 31) | function TooltipTrigger({
function TooltipContent (line 37) | function TooltipContent({
FILE: packages/gym/components/version-switcher.tsx
function VersionSwitcher (line 18) | function VersionSwitcher({
FILE: packages/gym/hooks/use-mobile.ts
constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768;
FILE: packages/gym/next.config.ts
method rewrites (line 4) | async rewrites() {
FILE: packages/gym/scripts/start-all-servers.js
constant PROVIDERS_WITH_SERVERS (line 9) | const PROVIDERS_WITH_SERVERS = [
FILE: packages/mcp/src/client.ts
type McpPluginOptions (line 4) | interface McpPluginOptions {
type Window (line 44) | interface Window {
constant MCP_REACHABLE_KEY (line 49) | const MCP_REACHABLE_KEY = "react-grab-mcp-reachable";
FILE: packages/mcp/src/constants.ts
constant CONTEXT_TTL_MS (line 1) | const CONTEXT_TTL_MS = 5 * 60 * 1000;
constant DEFAULT_MCP_PORT (line 2) | const DEFAULT_MCP_PORT = 4723;
constant HEALTH_CHECK_TIMEOUT_MS (line 3) | const HEALTH_CHECK_TIMEOUT_MS = 1000;
constant POST_KILL_DELAY_MS (line 4) | const POST_KILL_DELAY_MS = 100;
FILE: packages/mcp/src/server.ts
type AgentContext (line 25) | type AgentContext = z.infer<typeof agentContextSchema>;
type StoredContext (line 27) | interface StoredContext {
type McpSession (line 90) | interface McpSession {
type StartMcpServerOptions (line 229) | interface StartMcpServerOptions {
FILE: packages/provider-amp/src/handler.ts
type AmpAgentOptions (line 9) | interface AmpAgentOptions extends AgentRunOptions {}
type ThreadState (line 11) | interface ThreadState {
FILE: packages/provider-claude-code/src/handler.ts
type ClaudeAgentOptions (line 15) | interface ClaudeAgentOptions
type ContentBlock (line 18) | type ContentBlock = SDKAssistantMessage["message"]["content"][number];
type TextContentBlock (line 19) | type TextContentBlock = Extract<ContentBlock, { type: "text" }>;
FILE: packages/provider-codex/src/handler.ts
type CodexAgentOptions (line 9) | interface CodexAgentOptions extends AgentRunOptions {
type CodexThread (line 14) | type CodexThread = ReturnType<Codex["startThread"]>;
type ThreadState (line 16) | interface ThreadState {
type CodexEventItem (line 21) | interface CodexEventItem {
type CodexEvent (line 27) | interface CodexEvent {
FILE: packages/provider-copilot/src/handler.ts
type CopilotAgentOptions (line 10) | interface CopilotAgentOptions extends AgentRunOptions {
FILE: packages/provider-cursor/src/handler.ts
type CursorAgentOptions (line 10) | interface CursorAgentOptions extends AgentRunOptions {
type CursorStreamEvent (line 15) | interface CursorStreamEvent {
FILE: packages/provider-droid/src/handler.ts
type DroidAgentOptions (line 10) | interface DroidAgentOptions extends AgentRunOptions {
type DroidStreamEvent (line 17) | interface DroidStreamEvent {
FILE: packages/provider-gemini/src/handler.ts
type GeminiAgentOptions (line 10) | interface GeminiAgentOptions extends AgentRunOptions {
type GeminiStreamEvent (line 15) | interface GeminiStreamEvent {
FILE: packages/provider-opencode/src/constants.ts
constant OPENCODE_SDK_PORT (line 1) | const OPENCODE_SDK_PORT = 4096;
constant STATUS_TEXT_TRUNCATE_LENGTH (line 2) | const STATUS_TEXT_TRUNCATE_LENGTH = 100;
FILE: packages/provider-opencode/src/handler.ts
type OpenCodeAgentOptions (line 12) | interface OpenCodeAgentOptions extends AgentRunOptions {
type OpenCodeInstance (line 18) | interface OpenCodeInstance {
type OpenCodeEvent (line 23) | interface OpenCodeEvent {
type LastMessageInfo (line 39) | interface LastMessageInfo {
FILE: packages/react-grab/e2e/agent-resume-race.spec.ts
constant OLD_STREAM_ABORT_DELAY_MS (line 3) | const OLD_STREAM_ABORT_DELAY_MS = 150;
constant RESUME_STATUS_INTERVAL_MS (line 4) | const RESUME_STATUS_INTERVAL_MS = 40;
constant RACE_SETTLE_WAIT_MS (line 5) | const RACE_SETTLE_WAIT_MS = 500;
type ResumeRaceAgentActionContext (line 7) | interface ResumeRaceAgentActionContext {
type ResumeRaceAgentInstallerWindow (line 11) | interface ResumeRaceAgentInstallerWindow extends Window {
method send (line 55) | async *send(_context: unknown, signal: AbortSignal) {
method resume (line 59) | async *resume(_sessionId: string, signal: AbortSignal) {
FILE: packages/react-grab/e2e/context-menu.spec.ts
method send (line 558) | *send() {
method send (line 611) | *send() {
FILE: packages/react-grab/e2e/copy-feedback.spec.ts
constant FEEDBACK_DURATION_MS (line 3) | const FEEDBACK_DURATION_MS = 1500;
FILE: packages/react-grab/e2e/disabled-elements.spec.ts
constant CONTAINER_ID (line 3) | const CONTAINER_ID = "disabled-test-container";
FILE: packages/react-grab/e2e/fixtures.ts
constant ATTRIBUTE_NAME (line 3) | const ATTRIBUTE_NAME = "data-react-grab";
constant DEFAULT_KEY_HOLD_DURATION_MS (line 4) | const DEFAULT_KEY_HOLD_DURATION_MS = 200;
constant ACTIVATION_BUFFER_MS (line 5) | const ACTIVATION_BUFFER_MS = 200;
constant PAGE_SETUP_MAX_ATTEMPTS (line 6) | const PAGE_SETUP_MAX_ATTEMPTS = 2;
constant PAGE_SETUP_NAVIGATION_TIMEOUT_MS (line 7) | const PAGE_SETUP_NAVIGATION_TIMEOUT_MS = 8_000;
constant PAGE_SETUP_API_TIMEOUT_MS (line 8) | const PAGE_SETUP_API_TIMEOUT_MS = 8_000;
constant MODIFIER_KEY (line 9) | const MODIFIER_KEY = process.platform === "darwin" ? "Meta" : "Control";
type ContextMenuInfo (line 11) | interface ContextMenuInfo {
type SelectionLabelInfo (line 18) | interface SelectionLabelInfo {
type SelectionLabelBounds (line 27) | interface SelectionLabelBounds {
type ToolbarInfo (line 33) | interface ToolbarInfo {
type AgentSessionInfo (line 42) | interface AgentSessionInfo {
type LabelInstanceInfo (line 50) | interface LabelInstanceInfo {
type ReactGrabState (line 58) | interface ReactGrabState {
type GrabbedBoxInfo (line 73) | interface GrabbedBoxInfo {
type HistoryDropdownInfo (line 81) | interface HistoryDropdownInfo {
type ToolbarMenuInfo (line 86) | interface ToolbarMenuInfo {
type ReactGrabPageObject (line 92) | interface ReactGrabPageObject {
method send (line 1901) | async *send() {
FILE: packages/react-grab/e2e/focus-trap.spec.ts
constant FOCUS_TRAP_CONTAINER_ID (line 3) | const FOCUS_TRAP_CONTAINER_ID = "focus-trap-test-container";
FILE: packages/react-grab/e2e/freeze-animations.spec.ts
constant ATTRIBUTE_NAME (line 4) | const ATTRIBUTE_NAME = "data-react-grab";
FILE: packages/react-grab/e2e/history-reacquire.spec.ts
type ViewportRect (line 4) | interface ViewportRect {
FILE: packages/react-grab/e2e/ssr.spec.ts
constant DIRECTORY (line 6) | const DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
constant PACKAGE_DIRECTORY (line 7) | const PACKAGE_DIRECTORY = path.resolve(DIRECTORY, "..");
FILE: packages/react-grab/e2e/toggle-position-stability.spec.ts
constant POSITION_TOLERANCE_PX (line 4) | const POSITION_TOLERANCE_PX = 3;
constant TOGGLE_ANIMATION_SETTLE_MS (line 5) | const TOGGLE_ANIMATION_SETTLE_MS = 300;
FILE: packages/react-grab/e2e/toolbar-selection-hover.spec.ts
constant ATTRIBUTE_NAME (line 3) | const ATTRIBUTE_NAME = "data-react-grab";
FILE: packages/react-grab/scripts/css-rem-to-px.mjs
constant BROWSER_DEFAULT_FONT_SIZE_PX (line 14) | const BROWSER_DEFAULT_FONT_SIZE_PX = 16;
constant CSS_OUTPUT_PATH (line 15) | const CSS_OUTPUT_PATH = "./dist/styles.css";
FILE: packages/react-grab/scripts/postinstall.cjs
constant AUTOMATION_ENVIRONMENT_VARIABLE_NAMES (line 1) | const AUTOMATION_ENVIRONMENT_VARIABLE_NAMES = [
constant REACT_GRAB_INIT_COMMAND (line 11) | const REACT_GRAB_INIT_COMMAND = "npx -y grab@latest init";
constant INSTALL_HINT_MESSAGE (line 12) | const INSTALL_HINT_MESSAGE = `[react-grab] Package installed via automat...
FILE: packages/react-grab/src/components/clear-history-prompt.tsx
type ClearHistoryPromptProps (line 11) | interface ClearHistoryPromptProps {
FILE: packages/react-grab/src/components/context-menu.tsx
type ContextMenuProps (line 35) | interface ContextMenuProps {
type MenuItem (line 47) | interface MenuItem {
FILE: packages/react-grab/src/components/history-dropdown.tsx
constant ITEM_ACTION_CLASS (line 33) | const ITEM_ACTION_CLASS =
type HistoryDropdownProps (line 36) | interface HistoryDropdownProps {
FILE: packages/react-grab/src/components/icons/icon-check.tsx
type IconCheckProps (line 3) | interface IconCheckProps {
FILE: packages/react-grab/src/components/icons/icon-chevron.tsx
type IconChevronProps (line 3) | interface IconChevronProps {
FILE: packages/react-grab/src/components/icons/icon-clock.tsx
type IconClockProps (line 3) | interface IconClockProps {
FILE: packages/react-grab/src/components/icons/icon-copy.tsx
type IconCopyProps (line 3) | interface IconCopyProps {
FILE: packages/react-grab/src/components/icons/icon-ellipsis.tsx
type IconEllipsisProps (line 3) | interface IconEllipsisProps {
FILE: packages/react-grab/src/components/icons/icon-loader.tsx
type IconLoaderProps (line 3) | interface IconLoaderProps {
FILE: packages/react-grab/src/components/icons/icon-open.tsx
type IconOpenProps (line 3) | interface IconOpenProps {
FILE: packages/react-grab/src/components/icons/icon-reply.tsx
type IconReplyProps (line 3) | interface IconReplyProps {
FILE: packages/react-grab/src/components/icons/icon-retry.tsx
type IconRetryProps (line 3) | interface IconRetryProps {
FILE: packages/react-grab/src/components/icons/icon-return.tsx
type IconReturnProps (line 3) | interface IconReturnProps {
FILE: packages/react-grab/src/components/icons/icon-select.tsx
type IconSelectProps (line 3) | interface IconSelectProps {
FILE: packages/react-grab/src/components/icons/icon-submit.tsx
type IconSubmitProps (line 3) | interface IconSubmitProps {
FILE: packages/react-grab/src/components/icons/icon-trash.tsx
type IconTrashProps (line 3) | interface IconTrashProps {
FILE: packages/react-grab/src/components/overlay-canvas.tsx
constant LAYER_STYLES (line 29) | const LAYER_STYLES = {
type LayerName (line 52) | type LayerName = "drag" | "selection" | "grabbed" | "processing";
type OffscreenLayer (line 54) | interface OffscreenLayer {
type AnimatedBounds (line 59) | interface AnimatedBounds {
type OverlayCanvasProps (line 70) | interface OverlayCanvasProps {
FILE: packages/react-grab/src/components/selection-label/arrow-navigation-menu.tsx
type ArrowNavigationMenuProps (line 7) | interface ArrowNavigationMenuProps {
FILE: packages/react-grab/src/components/selection-label/completion-view.tsx
type MoreOptionsButtonProps (line 21) | interface MoreOptionsButtonProps {
FILE: packages/react-grab/src/components/selection-label/index.tsx
type LabelPosition (line 40) | interface LabelPosition {
constant DEFAULT_OFFSCREEN_POSITION (line 48) | const DEFAULT_OFFSCREEN_POSITION: LabelPosition = {
type PositionResult (line 56) | interface PositionResult {
FILE: packages/react-grab/src/components/toolbar/index.tsx
type ToolbarProps (line 75) | interface ToolbarProps {
type FreezeHandlersOptions (line 103) | interface FreezeHandlersOptions {
FILE: packages/react-grab/src/components/toolbar/state.ts
type SnapEdge (line 5) | type SnapEdge = "top" | "bottom" | "left" | "right";
constant STORAGE_KEY (line 7) | const STORAGE_KEY = "react-grab-toolbar-state";
FILE: packages/react-grab/src/components/toolbar/toolbar-content.tsx
type ToolbarContentProps (line 15) | interface ToolbarContentProps {
FILE: packages/react-grab/src/components/toolbar/toolbar-menu.tsx
type ToolbarMenuProps (line 17) | interface ToolbarMenuProps {
FILE: packages/react-grab/src/components/tooltip.tsx
type TooltipProps (line 16) | interface TooltipProps {
FILE: packages/react-grab/src/constants.ts
constant VERSION (line 3) | const VERSION = process.env.VERSION as string;
constant VIEWPORT_MARGIN_PX (line 5) | const VIEWPORT_MARGIN_PX = 8;
constant OFFSCREEN_POSITION (line 6) | const OFFSCREEN_POSITION = -1000;
constant SELECTION_LERP_FACTOR (line 8) | const SELECTION_LERP_FACTOR = 0.95;
constant FEEDBACK_DURATION_MS (line 10) | const FEEDBACK_DURATION_MS = 1500;
constant FADE_DURATION_MS (line 11) | const FADE_DURATION_MS = 100;
constant FADE_COMPLETE_BUFFER_MS (line 12) | const FADE_COMPLETE_BUFFER_MS = 150;
constant DISMISS_ANIMATION_BUFFER_MS (line 13) | const DISMISS_ANIMATION_BUFFER_MS = 50;
constant KEYDOWN_SPAM_TIMEOUT_MS (line 14) | const KEYDOWN_SPAM_TIMEOUT_MS = 200;
constant BLUR_DEACTIVATION_THRESHOLD_MS (line 15) | const BLUR_DEACTIVATION_THRESHOLD_MS = 500;
constant WINDOW_REFOCUS_GRACE_PERIOD_MS (line 16) | const WINDOW_REFOCUS_GRACE_PERIOD_MS = 200;
constant INPUT_FOCUS_ACTIVATION_DELAY_MS (line 17) | const INPUT_FOCUS_ACTIVATION_DELAY_MS = 400;
constant INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS (line 18) | const INPUT_TEXT_SELECTION_ACTIVATION_DELAY_MS = 600;
constant DEFAULT_KEY_HOLD_DURATION_MS (line 19) | const DEFAULT_KEY_HOLD_DURATION_MS = 100;
constant DEFAULT_MAX_CONTEXT_LINES (line 20) | const DEFAULT_MAX_CONTEXT_LINES = 3;
constant MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS (line 21) | const MIN_HOLD_FOR_ACTIVATION_AFTER_COPY_MS = 200;
constant RECENT_THRESHOLD_MS (line 22) | const RECENT_THRESHOLD_MS = 10_000;
constant FINDER_TIMEOUT_MS (line 23) | const FINDER_TIMEOUT_MS = 200;
constant SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS (line 24) | const SELECTOR_ATTR_VALUE_MAX_LENGTH_CHARS = 120;
constant ACTION_CYCLE_IDLE_TRIGGER_MS (line 26) | const ACTION_CYCLE_IDLE_TRIGGER_MS = 600;
constant DRAG_THRESHOLD_PX (line 28) | const DRAG_THRESHOLD_PX = 2;
constant ELEMENT_DETECTION_THROTTLE_MS (line 30) | const ELEMENT_DETECTION_THROTTLE_MS = 32;
constant PENDING_DETECTION_STALENESS_MS (line 31) | const PENDING_DETECTION_STALENESS_MS = 200;
constant COMPONENT_NAME_DEBOUNCE_MS (line 32) | const COMPONENT_NAME_DEBOUNCE_MS = 100;
constant DRAG_PREVIEW_DEBOUNCE_MS (line 33) | const DRAG_PREVIEW_DEBOUNCE_MS = 32;
constant BOUNDS_CACHE_TTL_MS (line 34) | const BOUNDS_CACHE_TTL_MS = 16;
constant BOUNDS_RECALC_INTERVAL_MS (line 35) | const BOUNDS_RECALC_INTERVAL_MS = 100;
constant AUTO_SCROLL_EDGE_THRESHOLD_PX (line 37) | const AUTO_SCROLL_EDGE_THRESHOLD_PX = 25;
constant AUTO_SCROLL_SPEED_PX (line 38) | const AUTO_SCROLL_SPEED_PX = 10;
constant Z_INDEX_HOST (line 40) | const Z_INDEX_HOST = 2147483647;
constant Z_INDEX_LABEL (line 41) | const Z_INDEX_LABEL = 2147483647;
constant Z_INDEX_OVERLAY_CANVAS (line 42) | const Z_INDEX_OVERLAY_CANVAS = 2147483645;
constant DRAG_LERP_FACTOR (line 44) | const DRAG_LERP_FACTOR = 0.7;
constant LERP_CONVERGENCE_THRESHOLD_PX (line 45) | const LERP_CONVERGENCE_THRESHOLD_PX = 0.5;
constant OPACITY_CONVERGENCE_THRESHOLD (line 46) | const OPACITY_CONVERGENCE_THRESHOLD = 0.01;
constant FADE_OUT_BUFFER_MS (line 47) | const FADE_OUT_BUFFER_MS = 100;
constant MIN_DEVICE_PIXEL_RATIO (line 48) | const MIN_DEVICE_PIXEL_RATIO = 2;
constant OVERLAY_BORDER_COLOR_DRAG (line 50) | const OVERLAY_BORDER_COLOR_DRAG = overlayColor(0.4);
constant OVERLAY_FILL_COLOR_DRAG (line 51) | const OVERLAY_FILL_COLOR_DRAG = overlayColor(0.05);
constant OVERLAY_BORDER_COLOR_DEFAULT (line 52) | const OVERLAY_BORDER_COLOR_DEFAULT = overlayColor(0.5);
constant OVERLAY_FILL_COLOR_DEFAULT (line 53) | const OVERLAY_FILL_COLOR_DEFAULT = overlayColor(0.08);
constant FROZEN_GLOW_COLOR (line 54) | const FROZEN_GLOW_COLOR = overlayColor(0.15);
constant FROZEN_GLOW_EDGE_PX (line 55) | const FROZEN_GLOW_EDGE_PX = 50;
constant ARROW_HEIGHT_PX (line 57) | const ARROW_HEIGHT_PX = 8;
constant ARROW_MIN_SIZE_PX (line 58) | const ARROW_MIN_SIZE_PX = 4;
constant ARROW_MAX_LABEL_WIDTH_RATIO (line 59) | const ARROW_MAX_LABEL_WIDTH_RATIO = 0.2;
constant ARROW_CENTER_PERCENT (line 60) | const ARROW_CENTER_PERCENT = 50;
constant ARROW_LABEL_MARGIN_PX (line 61) | const ARROW_LABEL_MARGIN_PX = 16;
constant LABEL_GAP_PX (line 62) | const LABEL_GAP_PX = 4;
constant PREVIEW_TEXT_MAX_LENGTH (line 63) | const PREVIEW_TEXT_MAX_LENGTH = 100;
constant PREVIEW_ATTR_VALUE_MAX_LENGTH (line 64) | const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15;
constant PREVIEW_MAX_ATTRS (line 65) | const PREVIEW_MAX_ATTRS = 3;
constant PREVIEW_PRIORITY_ATTRS (line 66) | const PREVIEW_PRIORITY_ATTRS: readonly string[] = [
constant MODIFIER_KEYS (line 76) | const MODIFIER_KEYS: readonly string[] = [
constant ARROW_KEYS (line 83) | const ARROW_KEYS = new Set([
constant FROZEN_ELEMENT_ATTRIBUTE (line 90) | const FROZEN_ELEMENT_ATTRIBUTE = "data-react-grab-frozen";
constant USER_IGNORE_ATTRIBUTE (line 92) | const USER_IGNORE_ATTRIBUTE = "data-react-grab-ignore";
constant VIEWPORT_COVERAGE_THRESHOLD (line 94) | const VIEWPORT_COVERAGE_THRESHOLD = 0.9;
constant OVERLAY_Z_INDEX_THRESHOLD (line 95) | const OVERLAY_Z_INDEX_THRESHOLD = 1000;
constant DEV_TOOLS_OVERLAY_Z_INDEX_THRESHOLD (line 96) | const DEV_TOOLS_OVERLAY_Z_INDEX_THRESHOLD = 2147483600;
constant TOOLTIP_DELAY_MS (line 98) | const TOOLTIP_DELAY_MS = 400;
constant TOOLTIP_GRACE_PERIOD_MS (line 99) | const TOOLTIP_GRACE_PERIOD_MS = 100;
constant TOOLBAR_SNAP_MARGIN_PX (line 101) | const TOOLBAR_SNAP_MARGIN_PX = 16;
constant TOOLBAR_FADE_IN_DELAY_MS (line 102) | const TOOLBAR_FADE_IN_DELAY_MS = 500;
constant TOOLBAR_SNAP_ANIMATION_DURATION_MS (line 103) | const TOOLBAR_SNAP_ANIMATION_DURATION_MS = 300;
constant TOOLBAR_DRAG_THRESHOLD_PX (line 104) | const TOOLBAR_DRAG_THRESHOLD_PX = 5;
constant TOOLBAR_VELOCITY_MULTIPLIER_MS (line 105) | const TOOLBAR_VELOCITY_MULTIPLIER_MS = 150;
constant TOOLBAR_COLLAPSED_SHORT_PX (line 106) | const TOOLBAR_COLLAPSED_SHORT_PX = 14;
constant TOOLBAR_COLLAPSED_LONG_PX (line 107) | const TOOLBAR_COLLAPSED_LONG_PX = 28;
constant TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS (line 108) | const TOOLBAR_COLLAPSE_ANIMATION_DURATION_MS = 150;
constant TOGGLE_ANIMATION_BUFFER_MS (line 109) | const TOGGLE_ANIMATION_BUFFER_MS = 50;
constant TOOLBAR_DEFAULT_WIDTH_PX (line 110) | const TOOLBAR_DEFAULT_WIDTH_PX = 78;
constant TOOLBAR_DEFAULT_HEIGHT_PX (line 111) | const TOOLBAR_DEFAULT_HEIGHT_PX = 28;
constant TOOLBAR_DEFAULT_POSITION_RATIO (line 112) | const TOOLBAR_DEFAULT_POSITION_RATIO = 0.5;
constant TOOLBAR_SHAKE_TOOLTIP_DURATION_MS (line 113) | const TOOLBAR_SHAKE_TOOLTIP_DURATION_MS = 1500;
constant SELECTION_HINT_CYCLE_INTERVAL_MS (line 114) | const SELECTION_HINT_CYCLE_INTERVAL_MS = 3000;
constant SELECTION_HINT_COUNT (line 115) | const SELECTION_HINT_COUNT = 3;
constant HINT_FLIP_IN_ANIMATION (line 117) | const HINT_FLIP_IN_ANIMATION =
constant DRAG_SELECTION_COVERAGE_THRESHOLD (line 120) | const DRAG_SELECTION_COVERAGE_THRESHOLD = 0.75;
constant DRAG_SELECTION_SAMPLE_SPACING_PX (line 121) | const DRAG_SELECTION_SAMPLE_SPACING_PX = 32;
constant DRAG_SELECTION_MIN_SAMPLES_PER_AXIS (line 122) | const DRAG_SELECTION_MIN_SAMPLES_PER_AXIS = 3;
constant DRAG_SELECTION_MAX_SAMPLES_PER_AXIS (line 123) | const DRAG_SELECTION_MAX_SAMPLES_PER_AXIS = 20;
constant DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS (line 124) | const DRAG_SELECTION_MAX_TOTAL_SAMPLE_POINTS = 100;
constant DRAG_SELECTION_EDGE_INSET_PX (line 125) | const DRAG_SELECTION_EDGE_INSET_PX = 1;
constant MAX_ARROW_NAVIGATION_HISTORY (line 127) | const MAX_ARROW_NAVIGATION_HISTORY = 50;
constant MAX_MEMORY_SESSIONS (line 128) | const MAX_MEMORY_SESSIONS = 50;
constant MAX_TRANSFORM_ANCESTOR_DEPTH (line 130) | const MAX_TRANSFORM_ANCESTOR_DEPTH = 6;
constant TRANSFORM_EARLY_BAIL_DEPTH (line 131) | const TRANSFORM_EARLY_BAIL_DEPTH = 3;
constant ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX (line 133) | const ELEMENT_POSITION_CACHE_DISTANCE_THRESHOLD_PX = 2;
constant ELEMENT_POSITION_THROTTLE_MS (line 134) | const ELEMENT_POSITION_THROTTLE_MS = 16;
constant POINTER_EVENTS_RESUME_DEBOUNCE_MS (line 135) | const POINTER_EVENTS_RESUME_DEBOUNCE_MS = 100;
constant VISIBILITY_CACHE_TTL_MS (line 136) | const VISIBILITY_CACHE_TTL_MS = 50;
constant ZOOM_DETECTION_THRESHOLD (line 138) | const ZOOM_DETECTION_THRESHOLD = 0.01;
constant MOUNT_ROOT_RECHECK_DELAY_MS (line 140) | const MOUNT_ROOT_RECHECK_DELAY_MS = 1000;
constant MAX_HISTORY_ITEMS (line 142) | const MAX_HISTORY_ITEMS = 20;
constant MAX_SESSION_STORAGE_SIZE_BYTES (line 143) | const MAX_SESSION_STORAGE_SIZE_BYTES = 2 * 1024 * 1024;
constant DROPDOWN_ANIMATION_DURATION_MS (line 144) | const DROPDOWN_ANIMATION_DURATION_MS = 100;
constant DROPDOWN_HOVER_OPEN_DELAY_MS (line 145) | const DROPDOWN_HOVER_OPEN_DELAY_MS = 200;
constant DROPDOWN_VIEWPORT_PADDING_PX (line 146) | const DROPDOWN_VIEWPORT_PADDING_PX = 8;
constant DROPDOWN_ANCHOR_GAP_PX (line 147) | const DROPDOWN_ANCHOR_GAP_PX = 8;
constant SAFE_POLYGON_BUFFER_PX (line 148) | const SAFE_POLYGON_BUFFER_PX = 8;
constant DROPDOWN_ICON_SIZE_PX (line 149) | const DROPDOWN_ICON_SIZE_PX = 11;
constant DROPDOWN_MIN_WIDTH_PX (line 150) | const DROPDOWN_MIN_WIDTH_PX = 180;
constant DROPDOWN_MAX_WIDTH_PX (line 151) | const DROPDOWN_MAX_WIDTH_PX = 280;
constant TOOLBAR_MENU_MIN_WIDTH_PX (line 152) | const TOOLBAR_MENU_MIN_WIDTH_PX = 100;
constant DROPDOWN_OFFSCREEN_POSITION (line 154) | const DROPDOWN_OFFSCREEN_POSITION = { left: -9999, top: -9999 };
constant DROPDOWN_EDGE_TRANSFORM_ORIGIN (line 156) | const DROPDOWN_EDGE_TRANSFORM_ORIGIN = {
constant NEXTJS_REVALIDATION_DELAY_MS (line 163) | const NEXTJS_REVALIDATION_DELAY_MS = 1000;
constant TEXTAREA_MAX_HEIGHT_PX (line 165) | const TEXTAREA_MAX_HEIGHT_PX = 95;
constant IME_COMPOSING_KEY_CODE (line 167) | const IME_COMPOSING_KEY_CODE = 229;
constant SELECTION_LABEL_OFFSCREEN_PX (line 168) | const SELECTION_LABEL_OFFSCREEN_PX = -9999;
constant RELEVANT_CSS_PROPERTIES (line 170) | const RELEVANT_CSS_PROPERTIES = new Set([
FILE: packages/react-grab/src/core/agent/manager.ts
type StartSessionParams (line 32) | interface StartSessionParams {
type AgentManagerHooks (line 41) | interface AgentManagerHooks {
type SessionOperations (line 48) | interface SessionOperations {
type HistoryOperations (line 60) | interface HistoryOperations {
type InternalOperations (line 65) | interface InternalOperations {
type AgentManager (line 71) | interface AgentManager {
FILE: packages/react-grab/src/core/agent/session.ts
constant STORAGE_KEY (line 12) | const STORAGE_KEY = "react-grab:agent-sessions";
FILE: packages/react-grab/src/core/arrow-navigation.ts
type ElementValidator (line 7) | interface ElementValidator {
type BoundsCalculator (line 11) | interface BoundsCalculator {
type ArrowNavigator (line 15) | interface ArrowNavigator {
FILE: packages/react-grab/src/core/auto-scroll.ts
type AutoScrollDirection (line 11) | interface AutoScrollDirection {
type AutoScroller (line 30) | interface AutoScroller {
FILE: packages/react-grab/src/core/context.ts
constant NON_COMPONENT_PREFIXES (line 31) | const NON_COMPONENT_PREFIXES = new Set([
constant NEXT_INTERNAL_COMPONENT_NAMES (line 42) | const NEXT_INTERNAL_COMPONENT_NAMES = new Set([
constant REACT_INTERNAL_COMPONENT_NAMES (line 70) | const REACT_INTERNAL_COMPONENT_NAMES = new Set([
type StackContextOptions (line 119) | interface StackContextOptions {
type FormatPriorityAttrsOptions (line 212) | interface FormatPriorityAttrsOptions {
FILE: packages/react-grab/src/core/copy.ts
type CopyOptions (line 6) | interface CopyOptions {
type CopyHooks (line 12) | interface CopyHooks {
FILE: packages/react-grab/src/core/events.ts
type EventListenerManager (line 1) | interface EventListenerManager {
FILE: packages/react-grab/src/core/index.tsx
type CopyWithLabelOptions (line 169) | interface CopyWithLabelOptions {
type BuildActionContextOptions (line 184) | interface BuildActionContextOptions {
method start (line 520) | start() {
method clear (line 530) | clear() {
FILE: packages/react-grab/src/core/keyboard-handlers.ts
type ModifierKeys (line 4) | interface ModifierKeys {
type PatchableGetter (line 18) | interface PatchableGetter {
type KeyDescriptor (line 23) | interface KeyDescriptor extends PropertyDescriptor {
type KeyboardEventClaimer (line 27) | interface KeyboardEventClaimer {
FILE: packages/react-grab/src/core/logo-svg.ts
constant LOGO_SVG (line 1) | const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" vi...
FILE: packages/react-grab/src/core/plugin-registry.ts
type RegisteredPlugin (line 30) | interface RegisteredPlugin {
type OptionsState (line 35) | interface OptionsState {
constant DEFAULT_OPTIONS (line 45) | const DEFAULT_OPTIONS: OptionsState = {
type PluginStoreState (line 55) | interface PluginStoreState {
type HookName (line 62) | type HookName = keyof PluginHooks;
FILE: packages/react-grab/src/core/plugins/create-pending-selection-plugin.ts
type ContextMenuActionFactory (line 8) | type ContextMenuActionFactory =
type PendingSelectionPluginConfig (line 12) | interface PendingSelectionPluginConfig {
FILE: packages/react-grab/src/core/store.ts
type PendingClickData (line 13) | interface PendingClickData {
type FrozenDragRect (line 19) | interface FrozenDragRect {
type GrabPhase (line 26) | type GrabPhase = "hovering" | "frozen" | "dragging" | "justDragged";
type GrabState (line 28) | type GrabState =
type GrabStore (line 40) | interface GrabStore {
type GrabStoreInput (line 91) | interface GrabStoreInput {
type GrabActions (line 148) | interface GrabActions {
FILE: packages/react-grab/src/core/theme.ts
constant DEFAULT_THEME (line 3) | const DEFAULT_THEME: Required<Theme> = {
FILE: packages/react-grab/src/index.ts
type Window (line 50) | interface Window {
FILE: packages/react-grab/src/primitives.ts
type ReactGrabElementContext (line 22) | interface ReactGrabElementContext {
FILE: packages/react-grab/src/types.ts
type Position (line 1) | interface Position {
type DeepPartial (line 6) | type DeepPartial<T> = {
type Theme (line 14) | interface Theme {
type ReactGrabState (line 77) | interface ReactGrabState {
type ElementLabelVariant (line 106) | type ElementLabelVariant = "hover" | "processing" | "success";
type PromptModeContext (line 108) | interface PromptModeContext {
type ElementLabelContext (line 114) | interface ElementLabelContext {
type ActivationKey (line 125) | type ActivationKey = string | ((event: KeyboardEvent) => boolean);
type AgentContext (line 127) | interface AgentContext<T = unknown> {
type AgentSession (line 134) | interface AgentSession {
type AgentProvider (line 149) | interface AgentProvider<T = unknown> {
type AgentSessionStorage (line 171) | interface AgentSessionStorage {
type AgentCompleteResult (line 177) | interface AgentCompleteResult {
type AgentOptions (line 181) | interface AgentOptions<T = unknown> {
type ActivationMode (line 198) | type ActivationMode = "toggle" | "hold";
type ActionContextHooks (line 200) | interface ActionContextHooks {
type ActionContext (line 210) | interface ActionContext {
type ContextMenuActionContext (line 224) | interface ContextMenuActionContext extends ActionContext {
type ContextMenuAction (line 228) | interface ContextMenuAction {
type ActionCycleItem (line 238) | interface ActionCycleItem {
type ActionCycleState (line 244) | interface ActionCycleState {
type ArrowNavigationItem (line 250) | interface ArrowNavigationItem {
type ArrowNavigationState (line 255) | interface ArrowNavigationState {
type PerformWithFeedbackOptions (line 261) | interface PerformWithFeedbackOptions {
type PluginHooks (line 267) | interface PluginHooks {
type ToolbarMenuAction (line 322) | interface ToolbarMenuAction {
type PluginAction (line 332) | type PluginAction = ContextMenuAction | ToolbarMenuAction;
type PluginConfig (line 334) | interface PluginConfig {
type Plugin (line 342) | interface Plugin {
type Options (line 351) | interface Options {
type SettableOptions (line 367) | interface SettableOptions extends Options {
type SourceInfo (line 371) | interface SourceInfo {
type ToolbarState (line 377) | interface ToolbarState {
type DropdownAnchor (line 384) | interface DropdownAnchor {
type ReactGrabAPI (line 391) | interface ReactGrabAPI {
type OverlayBounds (line 414) | interface OverlayBounds {
type SelectionLabelStatus (line 423) | type SelectionLabelStatus =
type SelectionLabelInstance (line 430) | interface SelectionLabelInstance {
type HistoryItem (line 451) | interface HistoryItem {
type ReactGrabRendererProps (line 465) | interface ReactGrabRendererProps {
type GrabbedBox (line 565) | interface GrabbedBox {
type Rect (line 572) | interface Rect {
type DragRect (line 579) | interface DragRect {
type ArrowPosition (line 586) | type ArrowPosition = "bottom" | "top";
type ArrowProps (line 588) | interface ArrowProps {
type TagBadgeProps (line 596) | interface TagBadgeProps {
type BottomSectionProps (line 606) | interface BottomSectionProps {
type DiscardPromptProps (line 610) | interface DiscardPromptProps {
type ErrorViewProps (line 617) | interface ErrorViewProps {
type CompletionViewProps (line 623) | interface CompletionViewProps {
type SelectionLabelProps (line 637) | interface SelectionLabelProps {
FILE: packages/react-grab/src/utils/combine-bounds.ts
type Bounds (line 1) | interface Bounds {
FILE: packages/react-grab/src/utils/copy-content.ts
constant REACT_GRAB_MIME_TYPE (line 3) | const REACT_GRAB_MIME_TYPE = "application/x-react-grab";
type ReactGrabEntry (line 5) | interface ReactGrabEntry {
type CopyContentOptions (line 12) | interface CopyContentOptions {
type ReactGrabMetadata (line 20) | interface ReactGrabMetadata {
FILE: packages/react-grab/src/utils/create-anchored-dropdown.ts
type AnchoredDropdownResult (line 16) | interface AnchoredDropdownResult {
FILE: packages/react-grab/src/utils/create-bounds-from-drag-rect.ts
type DragRectWithPageCoords (line 3) | interface DragRectWithPageCoords {
type BaseBounds (line 10) | interface BaseBounds {
FILE: packages/react-grab/src/utils/create-element-bounds.ts
type CachedBounds (line 12) | interface CachedBounds {
FILE: packages/react-grab/src/utils/create-element-selector.ts
constant PREFERRED_SELECTOR_ATTRIBUTE_NAMES (line 17) | const PREFERRED_SELECTOR_ATTRIBUTE_NAMES = new Set<string>([
FILE: packages/react-grab/src/utils/create-menu-highlight.ts
type AnimatedBoundsFollowerOptions (line 1) | interface AnimatedBoundsFollowerOptions {
type AnimatedBoundsFollowerController (line 6) | interface AnimatedBoundsFollowerController {
type MenuHighlightController (line 13) | interface MenuHighlightController {
constant DEFAULT_HIDDEN_OPACITY (line 20) | const DEFAULT_HIDDEN_OPACITY = "0";
constant DEFAULT_VISIBLE_OPACITY (line 21) | const DEFAULT_VISIBLE_OPACITY = "1";
FILE: packages/react-grab/src/utils/create-toolbar-drag.ts
type ToolbarDragConfig (line 16) | interface ToolbarDragConfig {
type ToolbarDragResult (line 32) | interface ToolbarDragResult {
FILE: packages/react-grab/src/utils/extract-element-css.ts
constant BORDER_FILTER_SIDE_MAP (line 3) | const BORDER_FILTER_SIDE_MAP = new Map(
FILE: packages/react-grab/src/utils/format-relative-time.ts
constant SECONDS_PER_MINUTE (line 1) | const SECONDS_PER_MINUTE = 60;
constant MINUTES_PER_HOUR (line 2) | const MINUTES_PER_HOUR = 60;
constant HOURS_PER_DAY (line 3) | const HOURS_PER_DAY = 24;
FILE: packages/react-grab/src/utils/freeze-animations.ts
constant FROZEN_STYLES (line 5) | const FROZEN_STYLES = `
constant GLOBAL_FREEZE_STYLES (line 13) | const GLOBAL_FREEZE_STYLES = `
constant SVG_ROOT_SELECTOR (line 20) | const SVG_ROOT_SELECTOR = "svg";
FILE: packages/react-grab/src/utils/freeze-pseudo-states.ts
constant POINTER_EVENTS_STYLES (line 4) | const POINTER_EVENTS_STYLES = "html { pointer-events: none !important; }";
constant MOUSE_EVENTS_TO_BLOCK (line 6) | const MOUSE_EVENTS_TO_BLOCK = [
constant FOCUS_EVENTS_TO_BLOCK (line 17) | const FOCUS_EVENTS_TO_BLOCK = ["focus", "blur", "focusin", "focusout"] a...
constant HOVER_STYLE_PROPERTIES (line 19) | const HOVER_STYLE_PROPERTIES = [
constant FOCUS_STYLE_PROPERTIES (line 32) | const FOCUS_STYLE_PROPERTIES = [
type FrozenPseudoState (line 48) | interface FrozenPseudoState {
FILE: packages/react-grab/src/utils/freeze-updates.ts
type FiberRootLike (line 12) | interface FiberRootLike extends FiberRoot {
type PendingUpdate (line 16) | interface PendingUpdate {
type HookQueue (line 22) | interface HookQueue {
type HookState (line 28) | interface HookState {
type ContextDependency (line 33) | interface ContextDependency {
type PausedQueueState (line 38) | interface PausedQueueState {
type PausedContextState (line 46) | interface PausedContextState {
type DispatchFunction (line 67) | type DispatchFunction = (...args: unknown[]) => void;
type TransitionFunction (line 68) | type TransitionFunction = (callback: () => void) => void;
type OriginalHooks (line 70) | interface OriginalHooks {
method get (line 265) | get() {
method set (line 272) | set(value: unknown) {
type UseSyncExternalStore (line 451) | type UseSyncExternalStore = <T>(
FILE: packages/react-grab/src/utils/generate-snippet.ts
type GenerateSnippetOptions (line 3) | interface GenerateSnippetOptions {
FILE: packages/react-grab/src/utils/get-anchored-dropdown-position.ts
type DropdownPosition (line 4) | interface DropdownPosition {
type GetAnchoredDropdownPositionOptions (line 9) | interface GetAnchoredDropdownPositionOptions {
FILE: packages/react-grab/src/utils/get-bounds-center.ts
type BoundsCenter (line 3) | interface BoundsCenter {
FILE: packages/react-grab/src/utils/get-element-at-position.ts
type PositionCache (line 12) | interface PositionCache {
FILE: packages/react-grab/src/utils/get-elements-in-drag.ts
type SamplePoint (line 51) | interface SamplePoint {
FILE: packages/react-grab/src/utils/get-tag-display.ts
type TagDisplayInput (line 1) | interface TagDisplayInput {
type TagDisplayOutput (line 7) | interface TagDisplayOutput {
FILE: packages/react-grab/src/utils/get-visible-bounds-center.ts
type Point (line 3) | interface Point {
FILE: packages/react-grab/src/utils/get-visual-viewport.ts
type VisualViewportInfo (line 1) | interface VisualViewportInfo {
FILE: packages/react-grab/src/utils/history-storage.ts
constant SESSION_STORAGE_KEY (line 9) | const SESSION_STORAGE_KEY = "react-grab-history-items";
FILE: packages/react-grab/src/utils/is-c-like-key.ts
constant C_LIKE_CHARACTERS (line 1) | const C_LIKE_CHARACTERS = new Set([
FILE: packages/react-grab/src/utils/is-keyboard-event-triggered-by-input.ts
constant EDITABLE_TAGS_AND_ROLES (line 3) | const EDITABLE_TAGS_AND_ROLES: readonly string[] = [
FILE: packages/react-grab/src/utils/is-target-key-combination.ts
type HotkeyOptions (line 6) | interface HotkeyOptions {
FILE: packages/react-grab/src/utils/is-valid-grabbable-element.ts
type VisibilityCache (line 70) | interface VisibilityCache {
FILE: packages/react-grab/src/utils/mount-root.ts
constant ATTRIBUTE_NAME (line 3) | const ATTRIBUTE_NAME = "data-react-grab";
constant FONT_LINK_ID (line 5) | const FONT_LINK_ID = "react-grab-fonts";
constant FONT_LINK_URL (line 6) | const FONT_LINK_URL =
FILE: packages/react-grab/src/utils/on-idle.ts
type BackgroundTaskScheduler (line 1) | interface BackgroundTaskScheduler {
type Window (line 9) | interface Window {
FILE: packages/react-grab/src/utils/open-file.ts
constant OPEN_FILE_BASE_URL (line 5) | const OPEN_FILE_BASE_URL =
FILE: packages/react-grab/src/utils/overlay-color.ts
constant SRGB_COMPONENTS (line 4) | const SRGB_COMPONENTS = "210, 57, 192";
constant P3_COMPONENTS (line 5) | const P3_COMPONENTS = "0.84 0.19 0.78";
FILE: packages/react-grab/src/utils/parse-activation-key.ts
type ParsedModifiers (line 5) | interface ParsedModifiers {
constant MODIFIER_MAP (line 13) | const MODIFIER_MAP: Record<string, keyof Omit<ParsedModifiers, "key">> = {
FILE: packages/react-grab/src/utils/recalculate-session-position.ts
type RecalculateSessionPositionOptions (line 4) | interface RecalculateSessionPositionOptions {
FILE: packages/react-grab/src/utils/register-overlay-dismiss.ts
type RegisterOverlayDismissOptions (line 8) | interface RegisterOverlayDismissOptions {
FILE: packages/react-grab/src/utils/safe-polygon.ts
type Point (line 1) | interface Point {
type TargetRect (line 6) | interface TargetRect {
FILE: packages/react-grab/src/utils/toolbar-position.ts
type Dimensions (line 105) | interface Dimensions {
type SnapResult (line 224) | interface SnapResult extends Position {
FILE: packages/react-grab/tsup.config.ts
constant DEFAULT_OPTIONS (line 36) | const DEFAULT_OPTIONS: Options = {
FILE: packages/relay/src/client.ts
type RelayClient (line 12) | interface RelayClient {
type RelayClientOptions (line 26) | interface RelayClientOptions {
type AgentProvider (line 244) | interface AgentProvider {
type CreateRelayAgentProviderOptions (line 254) | interface CreateRelayAgentProviderOptions {
type ReactGrabApi (line 483) | interface ReactGrabApi {
type Window (line 488) | interface Window {
type ProviderPluginConfig (line 513) | interface ProviderPluginConfig {
FILE: packages/relay/src/connection.ts
constant VERSION (line 13) | const VERSION = process.env.VERSION ?? "0.0.0";
type ConnectRelayOptions (line 15) | interface ConnectRelayOptions {
type RelayConnection (line 21) | interface RelayConnection {
FILE: packages/relay/src/protocol.ts
constant DEFAULT_RELAY_PORT (line 1) | const DEFAULT_RELAY_PORT = 4722;
constant DEFAULT_RECONNECT_INTERVAL_MS (line 2) | const DEFAULT_RECONNECT_INTERVAL_MS = 3000;
constant HEALTH_CHECK_TIMEOUT_MS (line 3) | const HEALTH_CHECK_TIMEOUT_MS = 1000;
constant POST_KILL_DELAY_MS (line 4) | const POST_KILL_DELAY_MS = 100;
constant RELAY_TOKEN_PARAM (line 5) | const RELAY_TOKEN_PARAM = "token";
constant COMPLETED_STATUS (line 6) | const COMPLETED_STATUS = "Completed";
type AgentMessage (line 8) | interface AgentMessage {
type AgentContext (line 13) | interface AgentContext {
type AgentRunOptions (line 20) | interface AgentRunOptions {
type AgentHandler (line 26) | interface AgentHandler {
type HandlerRegistrationMessage (line 37) | interface HandlerRegistrationMessage {
type HandlerUnregisterMessage (line 42) | interface HandlerUnregisterMessage {
type RelayToHandlerMessage (line 47) | interface RelayToHandlerMessage {
type HandlerToRelayMessage (line 57) | interface HandlerToRelayMessage {
type BrowserToRelayMessage (line 64) | interface BrowserToRelayMessage {
type RelayToBrowserMessage (line 76) | interface RelayToBrowserMessage {
type HandlerMessage (line 84) | type HandlerMessage =
type RelayMessage (line 89) | type RelayMessage = RelayToHandlerMessage | RelayToBrowserMessage;
FILE: packages/relay/src/server.ts
type RegisteredHandler (line 14) | interface RegisteredHandler {
type SessionMessageQueue (line 20) | interface SessionMessageQueue {
type ActiveSession (line 75) | interface ActiveSession {
type RelayServerOptions (line 83) | interface RelayServerOptions {
type RelayServer (line 88) | interface RelayServer {
FILE: packages/shadcn-registry/registry/react-grab.tsx
constant SCRIPT_ID (line 5) | const SCRIPT_ID = "react-grab-script";
constant SCRIPT_SRC (line 6) | const SCRIPT_SRC = "https://unpkg.com/react-grab/dist/index.global.js";
FILE: packages/shadcn-registry/scripts/build.js
constant CURRENT_DIRECTORY (line 5) | const CURRENT_DIRECTORY = dirname(fileURLToPath(import.meta.url));
constant ROOT_DIRECTORY (line 6) | const ROOT_DIRECTORY = resolve(CURRENT_DIRECTORY, "..");
constant OUTPUT_DIRECTORY (line 7) | const OUTPUT_DIRECTORY = resolve(ROOT_DIRECTORY, "r");
FILE: packages/utils/src/server.ts
constant COMMAND_INSTALL_MAP (line 4) | const COMMAND_INSTALL_MAP: Record<string, string> = {
type SpawnError (line 13) | interface SpawnError extends Error {
FILE: packages/web-extension/src/background/service-worker.ts
constant STORAGE_KEY (line 1) | const STORAGE_KEY = "react_grab_enabled";
FILE: packages/web-extension/src/constants.ts
constant LOCALHOST_INIT_DELAY_MS (line 1) | const LOCALHOST_INIT_DELAY_MS = 500;
constant STATE_QUERY_TIMEOUT_MS (line 2) | const STATE_QUERY_TIMEOUT_MS = 500;
FILE: packages/web-extension/src/content/react-grab.ts
type Window (line 10) | interface Window {
type ToolbarState (line 22) | interface ToolbarState {
type InitialState (line 153) | interface InitialState {
FILE: packages/website/app/api/og/route.tsx
constant BRAND_PINK (line 5) | const BRAND_PINK = "#fc4efd";
constant BACKGROUND_DARK_PURPLE (line 6) | const BACKGROUND_DARK_PURPLE = "#1a0815";
FILE: packages/website/app/api/report-cli/route.ts
type ReportPayload (line 3) | interface ReportPayload {
FILE: packages/website/app/blog/1-0/layout.tsx
type BlogPostLayoutProps (line 41) | interface BlogPostLayoutProps {
FILE: packages/website/app/blog/1-0/page.tsx
type ToolWithIconProps (line 18) | interface ToolWithIconProps {
FILE: packages/website/app/blog/agent/layout.tsx
type AgentLayoutProps (line 41) | interface AgentLayoutProps {
FILE: packages/website/app/blog/agent/page.tsx
type HighlightedCodeBlockProps (line 17) | interface HighlightedCodeBlockProps {
FILE: packages/website/app/blog/bets/layout.tsx
type BetsLayoutProps (line 40) | interface BetsLayoutProps {
FILE: packages/website/app/blog/intro/layout.tsx
type BlogPostLayoutProps (line 41) | interface BlogPostLayoutProps {
FILE: packages/website/app/blog/layout.tsx
type BlogLayoutProps (line 34) | interface BlogLayoutProps {
FILE: packages/website/app/blog/page.tsx
type BlogPost (line 8) | interface BlogPost {
FILE: packages/website/app/design-system/layout.tsx
type DesignSystemLayoutProps (line 43) | interface DesignSystemLayoutProps {
FILE: packages/website/app/open-file/layout.tsx
type OpenFileLayoutProps (line 34) | interface OpenFileLayoutProps {
FILE: packages/website/app/open-file/page.tsx
constant EDITOR_OPTIONS (line 15) | const EDITOR_OPTIONS = ["cursor", "vscode", "zed", "webstorm"] as const;
type Editor (line 16) | type Editor = (typeof EDITOR_OPTIONS)[number];
type EditorOption (line 18) | interface EditorOption {
constant EDITORS (line 24) | const EDITORS: EditorOption[] = [
constant STORAGE_KEY (line 31) | const STORAGE_KEY = "react-grab-preferred-editor";
FILE: packages/website/app/sitemap.ts
constant BASE_URL (line 5) | const BASE_URL = "https://react-grab.com";
constant EXCLUDED_PATHS (line 7) | const EXCLUDED_PATHS = new Set(["api", "open-file"]);
FILE: packages/website/components/benchmark-tooltip.tsx
type BenchmarkTooltipProps (line 16) | interface BenchmarkTooltipProps {
type MiniBarProps (line 22) | interface MiniBarProps {
type MiniChartProps (line 75) | interface MiniChartProps {
FILE: packages/website/components/benchmarks/benchmark-charts.tsx
type BenchmarkChartsProps (line 39) | interface BenchmarkChartsProps {
type CustomTooltipProps (line 43) | interface CustomTooltipProps {
type AnimatedBarProps (line 104) | interface AnimatedBarProps {
type AnimatedBarTreatmentProps (line 293) | interface AnimatedBarTreatmentProps {
type LiveCounterProps (line 352) | interface LiveCounterProps {
FILE: packages/website/components/benchmarks/benchmark-detailed-table.tsx
type BenchmarkDetailedTableProps (line 10) | interface BenchmarkDetailedTableProps {
type SortField (line 16) | type SortField =
type SortDirection (line 23) | type SortDirection = "asc" | "desc";
type SortIconProps (line 25) | interface SortIconProps {
type MetricColumn (line 31) | interface MetricColumn {
constant METRIC_COLUMNS (line 43) | const METRIC_COLUMNS: MetricColumn[] = [
constant HEADER_CLASS (line 101) | const HEADER_CLASS =
constant CONTROL_SUBHEADER_CLASS (line 103) | const CONTROL_SUBHEADER_CLASS =
constant TREATMENT_SUBHEADER_CLASS (line 105) | const TREATMENT_SUBHEADER_CLASS =
FILE: packages/website/components/benchmarks/types.ts
type BenchmarkResult (line 1) | interface BenchmarkResult {
type TestCase (line 12) | interface TestCase {
type GroupedResult (line 17) | interface GroupedResult {
type Metric (line 22) | interface Metric {
type ChangeInfo (line 30) | interface ChangeInfo {
type Stats (line 35) | interface Stats {
FILE: packages/website/components/blocks/grep-search-group.tsx
type ExploredHeaderProps (line 8) | interface ExploredHeaderProps {
type GrepSearchGroupProps (line 31) | interface GrepSearchGroupProps {
FILE: packages/website/components/blocks/grep-tool-call-block.tsx
type GrepToolCallBlockProps (line 4) | interface GrepToolCallBlockProps {
FILE: packages/website/components/blocks/message-block.tsx
type MessageBlockProps (line 7) | interface MessageBlockProps {
FILE: packages/website/components/blocks/read-tool-call-block.tsx
type ReadToolCallBlockProps (line 6) | interface ReadToolCallBlockProps {
FILE: packages/website/components/blocks/streaming-text.tsx
type StreamChunk (line 6) | interface StreamChunk {
type FadeInProps (line 11) | interface FadeInProps {
type StreamingChunksProps (line 40) | interface StreamingChunksProps {
type StreamingTextProps (line 67) | interface StreamingTextProps {
FILE: packages/website/components/blocks/thought-block.tsx
type ThoughtBlockProps (line 9) | interface ThoughtBlockProps {
FILE: packages/website/components/blog-article-layout.tsx
type TocHeading (line 9) | interface TocHeading {
type Author (line 15) | interface Author {
type BlogArticleLayoutProps (line 20) | interface BlogArticleLayoutProps {
FILE: packages/website/components/grab-element-button.tsx
type RecordedHotkey (line 21) | interface RecordedHotkey {
type SelectedElementInfo (line 29) | interface SelectedElementInfo {
type GrabElementButtonProps (line 40) | interface GrabElementButtonProps {
constant EMPTY_MODIFIERS (line 46) | const EMPTY_MODIFIERS: Readonly<Omit<RecordedHotkey, "key">> = {
type ReactGrabModule (line 53) | type ReactGrabModule = typeof import("react-grab");
type KbdProps (line 88) | interface KbdProps {
FILE: packages/website/components/homepage-demo.tsx
constant GREP_SEARCHES (line 50) | const GREP_SEARCHES = ["submit", "button", 'type="submit"'];
constant FALLBACK_ELEMENT (line 52) | const FALLBACK_ELEMENT = {
constant PATH_START_MARKERS (line 58) | const PATH_START_MARKERS = ["src", "components", "app", "pages"];
type BlockConfig (line 60) | interface BlockConfig {
type AgentEntry (line 108) | interface AgentEntry {
constant AGENTS (line 113) | const AGENTS: AgentEntry[] = [
type ElementAnalysisContentProps (line 184) | interface ElementAnalysisContentProps {
type ElementSelectContentProps (line 247) | interface ElementSelectContentProps {
constant BLOCK_CONFIGS (line 297) | const BLOCK_CONFIGS: BlockConfig[] = [
type RenderBlockProps (line 403) | interface RenderBlockProps {
constant GREP_INDEX (line 432) | const GREP_INDEX = BLOCK_CONFIGS.findIndex((c) => c.role === "grep");
constant GREP_ERROR_INDEX (line 433) | const GREP_ERROR_INDEX = BLOCK_CONFIGS.findIndex((c) => c.role === "grep...
FILE: packages/website/components/hotkey-context.tsx
type HotkeyContextValue (line 12) | interface HotkeyContextValue {
type HotkeyProviderProps (line 19) | interface HotkeyProviderProps {
FILE: packages/website/components/icons/icon-claude.tsx
type IconClaudeProps (line 1) | interface IconClaudeProps {
FILE: packages/website/components/icons/icon-codex.tsx
type IconCodexProps (line 1) | interface IconCodexProps {
FILE: packages/website/components/icons/icon-copilot.tsx
type IconCopilotProps (line 1) | interface IconCopilotProps {
FILE: packages/website/components/icons/icon-cursor.tsx
type IconCursorProps (line 1) | interface IconCursorProps {
FILE: packages/website/components/icons/icon-droid.tsx
type IconDroidProps (line 1) | interface IconDroidProps {
FILE: packages/website/components/icons/icon-github.tsx
type IconGithubProps (line 1) | interface IconGithubProps {
FILE: packages/website/components/icons/icon-nextjs.tsx
type IconNextjsProps (line 1) | interface IconNextjsProps {
FILE: packages/website/components/icons/icon-opencode.tsx
type IconOpenCodeProps (line 1) | interface IconOpenCodeProps {
FILE: packages/website/components/icons/icon-tanstack.tsx
type IconTanstackProps (line 1) | interface IconTanstackProps {
FILE: packages/website/components/icons/icon-vite.tsx
type IconViteProps (line 1) | interface IconViteProps {
FILE: packages/website/components/icons/icon-vscode.tsx
type IconVSCodeProps (line 1) | interface IconVSCodeProps {
FILE: packages/website/components/icons/icon-webstorm.tsx
type IconWebStormProps (line 1) | interface IconWebStormProps {
FILE: packages/website/components/icons/icon-zed.tsx
type IconZedProps (line 1) | interface IconZedProps {
FILE: packages/website/components/install-tabs.tsx
type InlineCodeProps (line 26) | interface InlineCodeProps {
type InstallTab (line 38) | interface InstallTab {
constant HEADING_TEXT_BY_VARIANT (line 242) | const HEADING_TEXT_BY_VARIANT: Record<InstallTab["variant"], string> = {
type InstallTabsProps (line 248) | interface InstallTabsProps {
FILE: packages/website/components/mobile-demo-animation.tsx
type Position (line 27) | interface Position {
type BoxState (line 34) | interface BoxState extends Position {
constant HIDDEN_BOX (line 38) | const HIDDEN_BOX: BoxState = {
type LabelState (line 46) | interface LabelState {
constant HIDDEN_LABEL (line 54) | const HIDDEN_LABEL: LabelState = {
type LabelMode (line 62) | type LabelMode =
type CursorType (line 70) | type CursorType = "default" | "crosshair" | "grabbing";
type HitElement (line 72) | interface HitElement {
constant METRIC_CARD_NAMES (line 78) | const METRIC_CARD_NAMES = ["RevenueCard", "UsersCard", "OrdersCard"];
constant ACTIVITY_DATA (line 80) | const ACTIVITY_DATA = [
constant INITIAL_CURSOR_POSITION (line 94) | const INITIAL_CURSOR_POSITION = { x: 150, y: 80 };
FILE: packages/website/components/react-grab-logo.tsx
constant BASE_ANIMATION_DURATION_MS (line 5) | const BASE_ANIMATION_DURATION_MS = 400;
constant SPEED_INCREMENT (line 6) | const SPEED_INCREMENT = 0.5;
type ReactGrabLogoProps (line 8) | interface ReactGrabLogoProps {
FILE: packages/website/components/table-of-contents.tsx
type TocHeading (line 5) | interface TocHeading {
type TableOfContentsProps (line 11) | interface TableOfContentsProps {
FILE: packages/website/components/ui/button.tsx
function Button (line 45) | function Button({
FILE: packages/website/components/ui/collapsible.tsx
type CollapsibleProps (line 7) | interface CollapsibleProps {
FILE: packages/website/components/ui/data-table-card.tsx
type DataTableCardProps (line 3) | interface DataTableCardProps {
FILE: packages/website/components/ui/scrollable.tsx
type ScrollableProps (line 5) | interface ScrollableProps {
FILE: packages/website/components/user-message.tsx
type UserMessageProps (line 7) | interface UserMessageProps {
FILE: packages/website/constants.ts
constant INITIAL_STREAM_DELAY_MS (line 1) | const INITIAL_STREAM_DELAY_MS = 100;
constant BLOCK_TRANSITION_DELAY_MS (line 2) | const BLOCK_TRANSITION_DELAY_MS = 50;
constant DEFAULT_CHUNK_SIZE (line 3) | const DEFAULT_CHUNK_SIZE = 4;
constant IMMEDIATE_TIMEOUT_MS (line 4) | const IMMEDIATE_TIMEOUT_MS = 0;
constant STREAM_DEMO_CHUNK_DELAY_MS (line 5) | const STREAM_DEMO_CHUNK_DELAY_MS = 20;
constant STREAM_DEMO_BLOCK_DELAY_MS (line 6) | const STREAM_DEMO_BLOCK_DELAY_MS = 400;
constant STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER (line 7) | const STREAM_DEMO_PRELOAD_ANIMATION_DELAY_MULTIPLIER = 0.03;
constant GREP_SEARCH_DELAY_MS (line 8) | const GREP_SEARCH_DELAY_MS = 400;
constant BENCHMARK_GRID_INTERVAL_SECONDS (line 10) | const BENCHMARK_GRID_INTERVAL_SECONDS = 5;
constant BENCHMARK_CHART_HEIGHT_PX (line 11) | const BENCHMARK_CHART_HEIGHT_PX = 320;
constant BENCHMARK_BAR_SIZE_PX (line 12) | const BENCHMARK_BAR_SIZE_PX = 40;
constant BENCHMARK_BAR_GAP_PX (line 13) | const BENCHMARK_BAR_GAP_PX = 12;
constant BENCHMARK_ANIMATION_DURATION_MS (line 14) | const BENCHMARK_ANIMATION_DURATION_MS = 1000;
constant BENCHMARK_CONTROL_COLOR (line 15) | const BENCHMARK_CONTROL_COLOR = "#525252";
constant BENCHMARK_TREATMENT_COLOR (line 16) | const BENCHMARK_TREATMENT_COLOR = "#ff4fff";
constant BENCHMARK_LIVE_COUNTER_INTERVAL_MS (line 17) | const BENCHMARK_LIVE_COUNTER_INTERVAL_MS = 50;
constant BENCHMARK_TOOLTIP_CONTROL_SECONDS (line 18) | const BENCHMARK_TOOLTIP_CONTROL_SECONDS = 16.8;
constant BENCHMARK_TOOLTIP_TREATMENT_SECONDS (line 19) | const BENCHMARK_TOOLTIP_TREATMENT_SECONDS = 5.8;
constant BENCHMARK_TOOLTIP_MAX_SECONDS (line 20) | const BENCHMARK_TOOLTIP_MAX_SECONDS = 20;
constant BENCHMARK_TOOLTIP_SPEEDUP_FACTOR (line 21) | const BENCHMARK_TOOLTIP_SPEEDUP_FACTOR = "3";
constant CLICK_FEEDBACK_DURATION_MS (line 23) | const CLICK_FEEDBACK_DURATION_MS = 300;
constant COPY_FEEDBACK_DURATION_MS (line 24) | const COPY_FEEDBACK_DURATION_MS = 1200;
constant TOOLTIP_HOVER_DELAY_MS (line 25) | const TOOLTIP_HOVER_DELAY_MS = 150;
constant HOTKEY_KEYUP_DELAY_MS (line 26) | const HOTKEY_KEYUP_DELAY_MS = 150;
constant CODE_BLOCK_COLLAPSE_LINE_THRESHOLD (line 27) | const CODE_BLOCK_COLLAPSE_LINE_THRESHOLD = 15;
constant CODE_BLOCK_MAX_HEIGHT_PX (line 28) | const CODE_BLOCK_MAX_HEIGHT_PX = 400;
constant PROMPT_INSTALL_COLLAPSE_LINE_THRESHOLD (line 29) | const PROMPT_INSTALL_COLLAPSE_LINE_THRESHOLD = 12;
constant PROMPT_INSTALL_MAX_HEIGHT_PX (line 30) | const PROMPT_INSTALL_MAX_HEIGHT_PX = 260;
constant TIMER_UPDATE_INTERVAL_MS (line 31) | const TIMER_UPDATE_INTERVAL_MS = 100;
constant AGENT_CYCLE_INTERVAL_MS (line 32) | const AGENT_CYCLE_INTERVAL_MS = 3000;
constant VIBRATION_DURATION_MS (line 34) | const VIBRATION_DURATION_MS = 100;
constant TAP_FEEDBACK_DISPLAY_MS (line 35) | const TAP_FEEDBACK_DISPLAY_MS = 800;
constant TAP_FEEDBACK_FADE_MS (line 36) | const TAP_FEEDBACK_FADE_MS = 300;
constant LABEL_OFFSET_BELOW_PX (line 37) | const LABEL_OFFSET_BELOW_PX = 10;
constant ANIMATION_RESTART_DELAY_MS (line 38) | const ANIMATION_RESTART_DELAY_MS = 200;
constant SELECTION_PADDING_PX (line 39) | const SELECTION_PADDING_PX = 4;
constant CURSOR_OFFSET_PX (line 40) | const CURSOR_OFFSET_PX = 16;
constant HINT_OVERLAY_DELAY_MS (line 41) | const HINT_OVERLAY_DELAY_MS = 3000;
constant IDLE_RESTART_DELAY_MS (line 42) | const IDLE_RESTART_DELAY_MS = 5000;
FILE: packages/website/hooks/use-stream.ts
type BlockContent (line 11) | type BlockContent = string | ReactNode | Array<string | ReactNode>;
type StreamStatus (line 23) | type StreamStatus = "pending" | "streaming" | "complete";
type StreamBlock (line 25) | interface StreamBlock {
type StreamChunk (line 39) | interface StreamChunk {
type StreamRenderedBlock (line 44) | interface StreamRenderedBlock {
type UseStreamOptions (line 62) | interface UseStreamOptions {
type StreamState (line 72) | interface StreamState {
type UseStreamReturn (line 81) | interface UseStreamReturn extends StreamState {
FILE: packages/website/instrumentation-client.ts
type Window (line 4) | interface Window {
FILE: packages/website/lib/api-helpers.ts
type HttpMethod (line 1) | type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
type CorsHeadersOptions (line 3) | interface CorsHeadersOptions {
FILE: packages/website/lib/shiki.ts
constant SHIKI_COLOR_OVERRIDES (line 7) | const SHIKI_COLOR_OVERRIDES: Record<string, string> = {
constant LINE_SPAN_REGEX (line 12) | const LINE_SPAN_REGEX = /<span class=("|')line\1>/g;
type HighlightCodeOptions (line 79) | interface HighlightCodeOptions {
FILE: packages/website/utils/get-key-from-code.ts
constant SPECIAL_KEY_SYMBOLS (line 1) | const SPECIAL_KEY_SYMBOLS: Record<string, string> = {
FILE: packages/website/utils/parse-changelog.ts
type ChangelogEntry (line 1) | interface ChangelogEntry {
Condensed preview — 499 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,482K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 743,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@3.0.3/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".github/workflows/code-quality.yml",
"chars": 961,
"preview": "name: Code Quality\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n lint:\n runs-on: ub"
},
{
"path": ".github/workflows/publish-any-commit.yml",
"chars": 964,
"preview": "name: Publish Any Commit\non: [push, pull_request]\n\npermissions: {}\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps"
},
{
"path": ".github/workflows/pullfrog.yml",
"chars": 1319,
"preview": "# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED\nname: Pullfrog\nrun-name: ${{ inputs.name || github.workflow }}\non"
},
{
"path": ".github/workflows/test-build.yml",
"chars": 527,
"preview": "name: Test Build\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n test-build:\n runs-on"
},
{
"path": ".github/workflows/test-cli.yml",
"chars": 600,
"preview": "name: Test CLI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n test-cli:\n runs-on: ub"
},
{
"path": ".github/workflows/test-e2e.yml",
"chars": 903,
"preview": "name: Test E2E\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n test-e2e:\n runs-on: ub"
},
{
"path": ".gitignore",
"chars": 223,
"preview": "node_modules\n.DS_Store\n.env\n.turbo\ndist\n.next\n**/*.tgz\ncoverage\nreact-grab-extension.zip\ntsup.config.bundled_*.mjs\npacka"
},
{
"path": ".oxfmtrc.json",
"chars": 247,
"preview": "{\n \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n \"tabWidth\": 2,\n \"singleQuote\": false,\n \"printWidth\""
},
{
"path": "AGENTS.md",
"chars": 7022,
"preview": "## General Rules\n\n- MUST: Use @antfu/ni. Use `ni` to install, `nr SCRIPT_NAME` to run. `nun` to uninstall.\n- MUST: Use T"
},
{
"path": "CONTRIBUTING.md",
"chars": 4154,
"preview": "# Contributing to React Grab\n\nThanks for your interest in contributing to React Grab! This document provides guidelines "
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2025 Aiden Bai\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 5830,
"preview": "# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center"
},
{
"path": "package.json",
"chars": 1733,
"preview": "{\n \"private\": true,\n \"workspaces\": [\n \"packages/*\"\n ],\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"cp README."
},
{
"path": "packages/cli/CHANGELOG.md",
"chars": 5402,
"preview": "# @react-grab/cli\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n\n## 0.1.27\n\n### Patch Changes\n\n- fix: install instructions\n\n## 0."
},
{
"path": "packages/cli/README.md",
"chars": 2243,
"preview": "# @react-grab/cli\n\nInteractive CLI to install React Grab in your project.\n\n## Usage\n\n```bash\nnpx grab\n```\n\n### Interacti"
},
{
"path": "packages/cli/package.json",
"chars": 788,
"preview": "{\n \"name\": \"@react-grab/cli\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab\": \"./dist/cli.js\"\n },\n \"files\": [\n "
},
{
"path": "packages/cli/src/cli.ts",
"chars": 865,
"preview": "import { Command } from \"commander\";\nimport { add } from \"./commands/add.js\";\nimport { configure } from \"./commands/conf"
},
{
"path": "packages/cli/src/commands/add.ts",
"chars": 15234,
"preview": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { detectNonInteractive } from \"../utils/is-non-"
},
{
"path": "packages/cli/src/commands/configure.ts",
"chars": 19942,
"preview": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { prompts } from \"../utils/prompts.js\";\nimport "
},
{
"path": "packages/cli/src/commands/init.ts",
"chars": 33141,
"preview": "import { existsSync } from \"node:fs\";\nimport { relative, resolve } from \"node:path\";\nimport { Command } from \"commander\""
},
{
"path": "packages/cli/src/commands/remove.ts",
"chars": 6947,
"preview": "import { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { detectNonInteractive } from \"../utils/is-non-"
},
{
"path": "packages/cli/src/utils/cli-helpers.ts",
"chars": 2409,
"preview": "import type { PackageManager } from \"./detect.js\";\nimport type {\n PackageJsonTransformResult,\n TransformResult,\n} from"
},
{
"path": "packages/cli/src/utils/constants.ts",
"chars": 124,
"preview": "export const MAX_SUGGESTIONS_COUNT = 30;\nexport const MAX_KEY_HOLD_DURATION_MS = 2000;\nexport const MAX_CONTEXT_LINES = "
},
{
"path": "packages/cli/src/utils/detect.ts",
"chars": 14202,
"preview": "import { execSync } from \"node:child_process\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport {"
},
{
"path": "packages/cli/src/utils/diff.ts",
"chars": 3909,
"preview": "interface DiffLine {\n type: \"added\" | \"removed\" | \"unchanged\";\n content: string;\n lineNumber?: number;\n}\n\nconst RED ="
},
{
"path": "packages/cli/src/utils/handle-error.ts",
"chars": 408,
"preview": "import { logger } from \"./logger.js\";\n\nexport const handleError = (error: unknown) => {\n logger.break();\n logger.error"
},
{
"path": "packages/cli/src/utils/highlighter.ts",
"chars": 151,
"preview": "import pc from \"picocolors\";\n\nexport const highlighter = {\n error: pc.red,\n warn: pc.yellow,\n info: pc.cyan,\n succes"
},
{
"path": "packages/cli/src/utils/install-mcp.ts",
"chars": 8300,
"preview": "import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nim"
},
{
"path": "packages/cli/src/utils/install.ts",
"chars": 1820,
"preview": "import { execSync } from \"node:child_process\";\nimport type { PackageManager } from \"./detect.js\";\nimport type { AgentInt"
},
{
"path": "packages/cli/src/utils/is-non-interactive.ts",
"chars": 411,
"preview": "const AGENT_ENVIRONMENT_VARIABLES = [\n \"CI\",\n \"CLAUDECODE\",\n \"CURSOR_AGENT\",\n \"CODEX_CI\",\n \"OPENCODE\",\n \"AMP_HOME\""
},
{
"path": "packages/cli/src/utils/logger.ts",
"chars": 611,
"preview": "import { highlighter } from \"./highlighter.js\";\n\nexport const logger = {\n error(...args: unknown[]) {\n console.log(h"
},
{
"path": "packages/cli/src/utils/prompts.ts",
"chars": 403,
"preview": "import basePrompts, { type PromptObject, type Answers } from \"prompts\";\nimport { logger } from \"./logger.js\";\n\nconst onC"
},
{
"path": "packages/cli/src/utils/spinner.ts",
"chars": 185,
"preview": "import ora from \"ora\";\n\ninterface SpinnerOptions {\n silent?: boolean;\n}\n\nexport const spinner = (text: string, options?"
},
{
"path": "packages/cli/src/utils/templates.ts",
"chars": 3557,
"preview": "export const AGENTS = [\n \"claude-code\",\n \"cursor\",\n \"opencode\",\n \"codex\",\n \"gemini\",\n \"amp\",\n \"droid\",\n \"copilot"
},
{
"path": "packages/cli/src/utils/transform.ts",
"chars": 42062,
"preview": "import {\n accessSync,\n constants,\n existsSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } fro"
},
{
"path": "packages/cli/test/configure.test.ts",
"chars": 21079,
"preview": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n previewOptionsTransform,\n applyOptionsTransfo"
},
{
"path": "packages/cli/test/detect.test.ts",
"chars": 11155,
"preview": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n detectFramework,\n detectMonorepo,\n detectNex"
},
{
"path": "packages/cli/test/diff.test.ts",
"chars": 4323,
"preview": "import { describe, expect, it } from \"vitest\";\nimport { generateDiff, formatDiff } from \"../src/utils/diff.js\";\n\ndescrib"
},
{
"path": "packages/cli/test/install-mcp.test.ts",
"chars": 8830,
"preview": "import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { afterEach, beforeEach, descri"
},
{
"path": "packages/cli/test/install.test.ts",
"chars": 2058,
"preview": "import { describe, expect, it } from \"vitest\";\nimport {\n getPackagesToInstall,\n getPackagesToUninstall,\n} from \"../src"
},
{
"path": "packages/cli/test/templates.test.ts",
"chars": 2830,
"preview": "import { describe, expect, it } from \"vitest\";\nimport {\n NEXT_APP_ROUTER_SCRIPT,\n NEXT_APP_ROUTER_SCRIPT_WITH_AGENT,\n "
},
{
"path": "packages/cli/test/transform.test.ts",
"chars": 31009,
"preview": "import { vi, describe, expect, it, beforeEach } from \"vitest\";\nimport {\n previewTransform,\n applyTransform,\n previewP"
},
{
"path": "packages/cli/tsconfig.json",
"chars": 283,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/cli/tsup.config.ts",
"chars": 579,
"preview": "import fs from \"node:fs\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JSON.parse(fs.readFileSync(\"package."
},
{
"path": "packages/cli/vitest.config.ts",
"chars": 200,
"preview": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n test: {\n globals: true,\n environmen"
},
{
"path": "packages/design-system/package.json",
"chars": 813,
"preview": "{\n \"name\": \"@react-grab/design-system\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"dist/in"
},
{
"path": "packages/design-system/src/index.tsx",
"chars": 98531,
"preview": "// @ts-expect-error - CSS imported as text via tsup loader\nimport cssText from \"react-grab/dist/styles.css\";\nimport { re"
},
{
"path": "packages/design-system/tsconfig.json",
"chars": 561,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"lib\": ["
},
{
"path": "packages/design-system/tsup.config.ts",
"chars": 923,
"preview": "import { defineConfig, type Options } from \"tsup\";\nimport babel from \"esbuild-plugin-babel\";\n\nconst options: Options = {"
},
{
"path": "packages/e2e-playground/index.html",
"chars": 312,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "packages/e2e-playground/package.json",
"chars": 596,
"preview": "{\n \"name\": \"@react-grab/e2e-playground\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \""
},
{
"path": "packages/e2e-playground/src/App.tsx",
"chars": 21231,
"preview": "import { useState, useRef, useEffect } from \"react\";\n\ninterface Todo {\n id: number;\n title: string;\n}\n\nconst TodoItem "
},
{
"path": "packages/e2e-playground/src/index.css",
"chars": 23,
"preview": "@import \"tailwindcss\";\n"
},
{
"path": "packages/e2e-playground/src/main.tsx",
"chars": 377,
"preview": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { init } from \"react-grab\";\nim"
},
{
"path": "packages/e2e-playground/tsconfig.json",
"chars": 626,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "packages/e2e-playground/tsconfig.node.json",
"chars": 213,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\""
},
{
"path": "packages/e2e-playground/vite.config.ts",
"chars": 308,
"preview": "import tailwindcss from \"@tailwindcss/vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vit"
},
{
"path": "packages/grab/CHANGELOG.md",
"chars": 6431,
"preview": "# grab\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - @react-grab/cli@0.1.28\n\n## 0.1.27\n\n### Patch Chan"
},
{
"path": "packages/grab/LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2025 Aiden Bai\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "packages/grab/README.md",
"chars": 5746,
"preview": "# <img src=\"https://github.com/aidenybai/react-grab/blob/main/.github/public/logo.png?raw=true\" width=\"60\" align=\"center"
},
{
"path": "packages/grab/bin/cli.js",
"chars": 46,
"preview": "#!/usr/bin/env node\nimport \"@react-grab/cli\";\n"
},
{
"path": "packages/grab/package.json",
"chars": 1647,
"preview": "{\n \"name\": \"grab\",\n \"version\": \"0.1.28\",\n \"description\": \"Select context for coding agents directly from your website"
},
{
"path": "packages/grab/scripts/build.js",
"chars": 4226,
"preview": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = pat"
},
{
"path": "packages/grab/tsconfig.json",
"chars": 188,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModul"
},
{
"path": "packages/gym/.gitignore",
"chars": 6,
"preview": ".next\n"
},
{
"path": "packages/gym/app/api/provider/[name]/route.ts",
"chars": 1175,
"preview": "import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { NextResponse, type NextRequest }"
},
{
"path": "packages/gym/app/dashboard/data.json",
"chars": 12795,
"preview": "[\n {\n \"id\": 1,\n \"header\": \"Cover page\",\n \"type\": \"Cover page\",\n \"status\": \"In Process\",\n \"target\": \"18\","
},
{
"path": "packages/gym/app/dashboard/page.tsx",
"chars": 1432,
"preview": "import { AppSidebar } from \"@/components/app-sidebar\";\nimport { ChartAreaInteractive } from \"@/components/chart-area-int"
},
{
"path": "packages/gym/app/freeze-demo/layout.tsx",
"chars": 122,
"preview": "export default function FreezeDemoLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return <>{children}</>;\n"
},
{
"path": "packages/gym/app/freeze-demo/page.tsx",
"chars": 3046,
"preview": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nconst TIMER_INTERVAL_MS = 10;\nconst VELOCITY_PX = 3"
},
{
"path": "packages/gym/app/globals.css",
"chars": 4186,
"preview": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-backg"
},
{
"path": "packages/gym/app/layout.tsx",
"chars": 990,
"preview": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport { ThemeProvider } fro"
},
{
"path": "packages/gym/app/login/page.tsx",
"chars": 275,
"preview": "import { LoginForm } from \"@/components/login-form\";\n\nexport default function Page() {\n return (\n <div className=\"fl"
},
{
"path": "packages/gym/app/page.tsx",
"chars": 106,
"preview": "import { redirect } from \"next/navigation\";\n\nexport default function Home() {\n redirect(\"/dashboard\");\n}\n"
},
{
"path": "packages/gym/app/playground/page.tsx",
"chars": 747,
"preview": "import { AppSidebar } from \"@/components/app-sidebar\";\nimport { SiteHeader } from \"@/components/site-header\";\nimport { S"
},
{
"path": "packages/gym/components/agent-playground.tsx",
"chars": 15445,
"preview": "\"use client\";\n\nimport { useEffect, useState, useRef, useCallback } from \"react\";\nimport { Button } from \"@/components/ui"
},
{
"path": "packages/gym/components/app-sidebar.tsx",
"chars": 3154,
"preview": "import * as React from \"react\";\n\nimport { SearchForm } from \"@/components/search-form\";\nimport { VersionSwitcher } from "
},
{
"path": "packages/gym/components/chart-area-interactive.tsx",
"chars": 10657,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\";\n\nimport"
},
{
"path": "packages/gym/components/counter.tsx",
"chars": 681,
"preview": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nconst COUNTER_INTERVAL_MS = 100;\n\nexport const Counter = ()"
},
{
"path": "packages/gym/components/data-table.tsx",
"chars": 26850,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n closestCenter,\n DndContext,\n KeyboardSensor,\n MouseSensor,\n"
},
{
"path": "packages/gym/components/login-form.tsx",
"chars": 2048,
"preview": "import { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Card,\n CardContent,\n Car"
},
{
"path": "packages/gym/components/nav-user.tsx",
"chars": 3356,
"preview": "\"use client\";\n\nimport {\n IconCreditCard,\n IconDotsVertical,\n IconLogout,\n IconNotification,\n IconUserCircle,\n} from"
},
{
"path": "packages/gym/components/search-form.tsx",
"chars": 805,
"preview": "import { Search } from \"lucide-react\";\n\nimport { Label } from \"@/components/ui/label\";\nimport {\n SidebarGroup,\n Sideba"
},
{
"path": "packages/gym/components/section-cards.tsx",
"chars": 3737,
"preview": "import { IconTrendingDown, IconTrendingUp } from \"@tabler/icons-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\n"
},
{
"path": "packages/gym/components/sheet-demo.tsx",
"chars": 1373,
"preview": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\""
},
{
"path": "packages/gym/components/site-header.tsx",
"chars": 1252,
"preview": "import { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Sidebar"
},
{
"path": "packages/gym/components/theme-provider.tsx",
"chars": 303,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexpor"
},
{
"path": "packages/gym/components/theme-toggle.tsx",
"chars": 1247,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-"
},
{
"path": "packages/gym/components/ui/avatar.tsx",
"chars": 1107,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn }"
},
{
"path": "packages/gym/components/ui/badge.tsx",
"chars": 1642,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
},
{
"path": "packages/gym/components/ui/button.tsx",
"chars": 2228,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
},
{
"path": "packages/gym/components/ui/card.tsx",
"chars": 2000,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.Compone"
},
{
"path": "packages/gym/components/ui/chart.tsx",
"chars": 10144,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib"
},
{
"path": "packages/gym/components/ui/checkbox.tsx",
"chars": 1227,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { C"
},
{
"path": "packages/gym/components/ui/drawer.tsx",
"chars": 4273,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/"
},
{
"path": "packages/gym/components/ui/dropdown-menu.tsx",
"chars": 8457,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\ni"
},
{
"path": "packages/gym/components/ui/field.tsx",
"chars": 6196,
"preview": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimpo"
},
{
"path": "packages/gym/components/ui/input.tsx",
"chars": 967,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React."
},
{
"path": "packages/gym/components/ui/label.tsx",
"chars": 618,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\n\nimport { cn } f"
},
{
"path": "packages/gym/components/ui/select.tsx",
"chars": 6375,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check"
},
{
"path": "packages/gym/components/ui/separator.tsx",
"chars": 706,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport "
},
{
"path": "packages/gym/components/ui/sheet.tsx",
"chars": 4109,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { XIcon "
},
{
"path": "packages/gym/components/ui/sidebar.tsx",
"chars": 21832,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantPr"
},
{
"path": "packages/gym/components/ui/skeleton.tsx",
"chars": 279,
"preview": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n"
},
{
"path": "packages/gym/components/ui/table.tsx",
"chars": 2464,
"preview": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Table({ className, ...props "
},
{
"path": "packages/gym/components/ui/tabs.tsx",
"chars": 1980,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } fro"
},
{
"path": "packages/gym/components/ui/toggle-group.tsx",
"chars": 2329,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\";\nimp"
},
{
"path": "packages/gym/components/ui/toggle.tsx",
"chars": 1579,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, "
},
{
"path": "packages/gym/components/ui/tooltip.tsx",
"chars": 1902,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn"
},
{
"path": "packages/gym/components/version-switcher.tsx",
"chars": 1995,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, ChevronsUpDown, GalleryVerticalEnd } from \"lucide-react\";"
},
{
"path": "packages/gym/components.json",
"chars": 447,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "packages/gym/hooks/use-mobile.ts",
"chars": 598,
"preview": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport const useIsMobile = (): boolean => {\n const [is"
},
{
"path": "packages/gym/instrumentation-client.ts",
"chars": 11,
"preview": "export {};\n"
},
{
"path": "packages/gym/lib/utils.ts",
"chars": 173,
"preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport const cn = (...inputs: C"
},
{
"path": "packages/gym/next-env.d.ts",
"chars": 211,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "packages/gym/next.config.ts",
"chars": 265,
"preview": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n async rewrites() {\n return [\n {\n "
},
{
"path": "packages/gym/package.json",
"chars": 3499,
"preview": "{\n \"name\": \"@react-grab/gym\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"n"
},
{
"path": "packages/gym/postcss.config.mjs",
"chars": 94,
"preview": "const config = {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n};\n\nexport default config;\n"
},
{
"path": "packages/gym/scripts/start-all-servers.js",
"chars": 1941,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"child_process\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join }"
},
{
"path": "packages/gym/tsconfig.json",
"chars": 649,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n "
},
{
"path": "packages/mcp/CHANGELOG.md",
"chars": 1648,
"preview": "# @react-grab/mcp\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n\n## 0.1.27\n\n### Patc"
},
{
"path": "packages/mcp/package.json",
"chars": 879,
"preview": "{\n \"name\": \"@react-grab/mcp\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-mcp\": \"./dist/cli.cjs\"\n },\n \"files\": "
},
{
"path": "packages/mcp/src/cli.ts",
"chars": 178,
"preview": "#!/usr/bin/env node\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer({\n port: Number(process.env.PORT) || "
},
{
"path": "packages/mcp/src/client.ts",
"chars": 2765,
"preview": "import type { init, ReactGrabAPI, Plugin, AgentContext } from \"react-grab/core\";\nimport { DEFAULT_MCP_PORT, HEALTH_CHECK"
},
{
"path": "packages/mcp/src/constants.ts",
"chars": 167,
"preview": "export const CONTEXT_TTL_MS = 5 * 60 * 1000;\nexport const DEFAULT_MCP_PORT = 4723;\nexport const HEALTH_CHECK_TIMEOUT_MS "
},
{
"path": "packages/mcp/src/server.ts",
"chars": 7226,
"preview": "import { randomUUID } from \"node:crypto\";\nimport { createServer, type Server } from \"node:http\";\nimport { McpServer } fr"
},
{
"path": "packages/mcp/tsconfig.json",
"chars": 306,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/mcp/tsup.config.ts",
"chars": 1105,
"preview": "import module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n {\n entry: {\n "
},
{
"path": "packages/provider-amp/CHANGELOG.md",
"chars": 8635,
"preview": "# @react-grab/amp\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/rela"
},
{
"path": "packages/provider-amp/README.md",
"chars": 888,
"preview": "# @react-grab/amp\n\n[Amp](https://ampcode.com) provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab"
},
{
"path": "packages/provider-amp/package.json",
"chars": 1071,
"preview": "{\n \"name\": \"@react-grab/amp\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-amp\": \"./dist/cli.cjs\"\n },\n \"files\": "
},
{
"path": "packages/provider-amp/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-amp/src/client.ts",
"chars": 455,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-amp/src/handler.ts",
"chars": 4488,
"preview": "import { execute } from \"@sourcegraph/amp-sdk\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} from "
},
{
"path": "packages/provider-amp/src/server.ts",
"chars": 191,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { ampAgentHandler } from \"./handler.js\";\n\nexport const s"
},
{
"path": "packages/provider-amp/tsconfig.json",
"chars": 307,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModul"
},
{
"path": "packages/provider-amp/tsup.config.ts",
"chars": 1382,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-claude-code/CHANGELOG.md",
"chars": 11321,
"preview": "# @react-grab/claude-code\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-g"
},
{
"path": "packages/provider-claude-code/README.md",
"chars": 3164,
"preview": "# @react-grab/claude-code\n\nClaude Code agent provider for React Grab. Requires running a local server that interfaces wi"
},
{
"path": "packages/provider-claude-code/package.json",
"chars": 1073,
"preview": "{\n \"name\": \"@react-grab/claude-code\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-claude-code\": \"./dist/cli.cjs\"\n"
},
{
"path": "packages/provider-claude-code/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-claude-code/src/client.ts",
"chars": 488,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-claude-code/src/handler.ts",
"chars": 5145,
"preview": "import { execSync } from \"node:child_process\";\nimport {\n query,\n type Options,\n type SDKAssistantMessage,\n} from \"@an"
},
{
"path": "packages/provider-claude-code/src/server.ts",
"chars": 205,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { claudeAgentHandler } from \"./handler.js\";\n\nexport cons"
},
{
"path": "packages/provider-claude-code/tsconfig.json",
"chars": 306,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/provider-claude-code/tsup.config.ts",
"chars": 1389,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-codex/CHANGELOG.md",
"chars": 8834,
"preview": "# @react-grab/codex\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/re"
},
{
"path": "packages/provider-codex/README.md",
"chars": 827,
"preview": "# @react-grab/codex\n\nOpenAI Codex provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab/codex\n```\n\n"
},
{
"path": "packages/provider-codex/package.json",
"chars": 1049,
"preview": "{\n \"name\": \"@react-grab/codex\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-codex\": \"./dist/cli.cjs\"\n },\n \"file"
},
{
"path": "packages/provider-codex/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-codex/src/client.ts",
"chars": 467,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-codex/src/handler.ts",
"chars": 4340,
"preview": "import { Codex } from \"@openai/codex-sdk\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} from \"@rea"
},
{
"path": "packages/provider-codex/src/server.ts",
"chars": 197,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { codexAgentHandler } from \"./handler.js\";\n\nexport const"
},
{
"path": "packages/provider-codex/tsconfig.json",
"chars": 307,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModul"
},
{
"path": "packages/provider-codex/tsup.config.ts",
"chars": 1384,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-copilot/CHANGELOG.md",
"chars": 1637,
"preview": "# @react-grab/copilot\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/"
},
{
"path": "packages/provider-copilot/package.json",
"chars": 1040,
"preview": "{\n \"name\": \"@react-grab/copilot\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-copilot\": \"./dist/cli.cjs\"\n },\n \""
},
{
"path": "packages/provider-copilot/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-copilot/src/client.ts",
"chars": 479,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-copilot/src/handler.ts",
"chars": 6069,
"preview": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} fr"
},
{
"path": "packages/provider-copilot/src/server.ts",
"chars": 203,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { copilotAgentHandler } from \"./handler.js\";\n\nexport con"
},
{
"path": "packages/provider-copilot/tsconfig.json",
"chars": 306,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/provider-copilot/tsup.config.ts",
"chars": 1386,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-cursor/CHANGELOG.md",
"chars": 11316,
"preview": "# @react-grab/cursor\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/r"
},
{
"path": "packages/provider-cursor/README.md",
"chars": 3108,
"preview": "# @react-grab/cursor\n\nCursor agent provider for React Grab. Requires running a local server that interfaces with the Cur"
},
{
"path": "packages/provider-cursor/package.json",
"chars": 1038,
"preview": "{\n \"name\": \"@react-grab/cursor\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-cursor\": \"./dist/cli.cjs\"\n },\n \"fi"
},
{
"path": "packages/provider-cursor/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-cursor/src/client.ts",
"chars": 473,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-cursor/src/handler.ts",
"chars": 9058,
"preview": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} fr"
},
{
"path": "packages/provider-cursor/src/server.ts",
"chars": 200,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { cursorAgentHandler } from \"./handler.js\";\n\nexport cons"
},
{
"path": "packages/provider-cursor/tsconfig.json",
"chars": 306,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/provider-cursor/tsup.config.ts",
"chars": 1385,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-droid/CHANGELOG.md",
"chars": 7740,
"preview": "# @react-grab/droid\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/re"
},
{
"path": "packages/provider-droid/README.md",
"chars": 3874,
"preview": "# @react-grab/droid\n\nFactory Droid provider for React Grab. Requires running a local server that interfaces with the Fac"
},
{
"path": "packages/provider-droid/package.json",
"chars": 1036,
"preview": "{\n \"name\": \"@react-grab/droid\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-droid\": \"./dist/cli.cjs\"\n },\n \"file"
},
{
"path": "packages/provider-droid/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-droid/src/client.ts",
"chars": 467,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-droid/src/handler.ts",
"chars": 8360,
"preview": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} fr"
},
{
"path": "packages/provider-droid/src/server.ts",
"chars": 197,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { droidAgentHandler } from \"./handler.js\";\n\nexport const"
},
{
"path": "packages/provider-droid/tsconfig.json",
"chars": 306,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\""
},
{
"path": "packages/provider-droid/tsup.config.ts",
"chars": 1384,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-gemini/CHANGELOG.md",
"chars": 8819,
"preview": "# @react-grab/gemini\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab/r"
},
{
"path": "packages/provider-gemini/README.md",
"chars": 867,
"preview": "# @react-grab/gemini\n\nGoogle Gemini CLI provider for React Grab.\n\n## Installation\n\n```bash\nnpm install @react-grab/gemin"
},
{
"path": "packages/provider-gemini/package.json",
"chars": 1038,
"preview": "{\n \"name\": \"@react-grab/gemini\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-gemini\": \"./dist/cli.cjs\"\n },\n \"fi"
},
{
"path": "packages/provider-gemini/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-gemini/src/client.ts",
"chars": 473,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-gemini/src/handler.ts",
"chars": 8166,
"preview": "import { execa, type ResultPromise } from \"execa\";\nimport type {\n AgentHandler,\n AgentMessage,\n AgentRunOptions,\n} fr"
},
{
"path": "packages/provider-gemini/src/server.ts",
"chars": 200,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { geminiAgentHandler } from \"./handler.js\";\n\nexport cons"
},
{
"path": "packages/provider-gemini/tsconfig.json",
"chars": 307,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModul"
},
{
"path": "packages/provider-gemini/tsup.config.ts",
"chars": 1385,
"preview": "import fs from \"node:fs\";\nimport module from \"node:module\";\nimport { defineConfig } from \"tsup\";\n\nconst packageJson = JS"
},
{
"path": "packages/provider-opencode/CHANGELOG.md",
"chars": 9985,
"preview": "# @react-grab/opencode\n\n## 0.1.28\n\n### Patch Changes\n\n- fix\n- Updated dependencies\n - react-grab@0.1.28\n - @react-grab"
},
{
"path": "packages/provider-opencode/README.md",
"chars": 3683,
"preview": "# @react-grab/opencode\n\nOpenCode agent provider for React Grab. Requires running a local server that interfaces with the"
},
{
"path": "packages/provider-opencode/package.json",
"chars": 1078,
"preview": "{\n \"name\": \"@react-grab/opencode\",\n \"version\": \"0.1.28\",\n \"bin\": {\n \"react-grab-opencode\": \"./dist/cli.cjs\"\n },\n "
},
{
"path": "packages/provider-opencode/src/cli.ts",
"chars": 490,
"preview": "#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { realpathSync } from \"node:fs\";\nimport { dirname"
},
{
"path": "packages/provider-opencode/src/client.ts",
"chars": 485,
"preview": "import type { AgentCompleteResult } from \"react-grab/core\";\nimport { createProviderClientPlugin } from \"@react-grab/rela"
},
{
"path": "packages/provider-opencode/src/constants.ts",
"chars": 87,
"preview": "export const OPENCODE_SDK_PORT = 4096;\nexport const STATUS_TEXT_TRUNCATE_LENGTH = 100;\n"
},
{
"path": "packages/provider-opencode/src/handler.ts",
"chars": 7527,
"preview": "import { createOpencode } from \"@opencode-ai/sdk\";\nimport fkill from \"fkill\";\nimport type {\n AgentHandler,\n AgentMessa"
},
{
"path": "packages/provider-opencode/src/server.ts",
"chars": 206,
"preview": "import { startProviderServer } from \"@react-grab/relay\";\nimport { openCodeAgentHandler } from \"./handler.js\";\n\nexport co"
}
]
// ... and 299 more files (download for full content)
About this extraction
This page contains the full source code of the aidenybai/react-grab GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 499 files (2.2 MB), approximately 608.2k tokens, and a symbol index with 893 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.