Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9cd3020
feat(mcp): read element context from the OS clipboard
aidenybai Apr 25, 2026
a4b36e5
fix(mcp): decode clipboard data delivered as a System.IO.Stream on Wi…
aidenybai Apr 25, 2026
0874363
fix(mcp): deduplicate prompt and stop embedding it in the elements se…
aidenybai Apr 25, 2026
c70f029
fix(mcp): widen Wayland-to-X11 fallback and preserve hints across WSL…
aidenybai Apr 25, 2026
1ad39d7
fix(mcp): preserve canonical content formatting and avoid PowerShell …
aidenybai Apr 25, 2026
0758344
fix(mcp): strip prompt prefix using the untrimmed commentText
aidenybai Apr 25, 2026
1a0e80c
fix(mcp): match prompt prefix only against the raw commentText
aidenybai Apr 25, 2026
03b722a
feat(cli): replace MCP with `react-grab watch` CLI + agent skill inst…
aidenybai Apr 26, 2026
39d1534
fix(cli): address cubic review on the watch CLI + skill installer
aidenybai Apr 26, 2026
4ec019f
fix(cli): address second cubic + bugbot review pass
aidenybai Apr 26, 2026
8fdc2c8
web
aidenybai Apr 26, 2026
fcc4871
web
aidenybai Apr 26, 2026
1d12d7b
fix(cli): address bugbot WSL recoverable + last-selected persist on s…
aidenybai Apr 26, 2026
2b21723
fix(cli): add command also passes projectInfo.projectRoot to skill in…
aidenybai Apr 26, 2026
c0ba21a
fix(cli): flush stderr before exit on watch error paths
aidenybai Apr 26, 2026
e91b8b3
fix(init): skill install failure does not abort the React Grab install
aidenybai Apr 26, 2026
fc50233
fix(cli): two more bugbot findings
aidenybai Apr 26, 2026
e9b2c3e
fix(macos): decode Chromium web-custom-data pickle so the reader actu…
aidenybai Apr 26, 2026
5129a05
fix(cli): tighten Chromium pickle decoder per code review
aidenybai Apr 26, 2026
d7d9fca
fix(cli): two more bugbot findings on remove + last-selected persistence
aidenybai Apr 26, 2026
aa8daba
fix(cli): walk up to project root for install-skill, remove, add
aidenybai Apr 26, 2026
4a4144b
fix(cli): four more bugbot findings on installer + WSL reader
aidenybai Apr 26, 2026
5b745a7
feat(cli): add check-installed (alias is-installed) + tighten skill t…
aidenybai Apr 26, 2026
abb3992
fix(cli): add command exits 1 on real skill install failure
aidenybai Apr 26, 2026
3927008
fix(cli): check-installed walks up to project root
aidenybai Apr 26, 2026
b3bb3ed
fix: thread extraPrompt to commentText for getContent payloads + READ…
aidenybai Apr 26, 2026
0df13a2
docs(cli): drop legacy 'grab add mcp' deprecation note from README
aidenybai Apr 26, 2026
b1cd9cf
fix(cli): detect react-grab in monorepo workspaces and source repo
aidenybai Apr 26, 2026
f9ea9e5
fix(cli): seven more bugbot findings
aidenybai Apr 26, 2026
3a86644
fix(cli): align check-installed with watch.ts patterns; tighten comme…
aidenybai Apr 26, 2026
f448bc4
fix(cli): three more bugbot findings on init, watch, and skill prompt
aidenybai Apr 26, 2026
c64f05f
fix(cli): two more bugbot findings on cancellation parity + WSL hint
aidenybai Apr 26, 2026
4840457
chore(cli): single-source the skill markdown via symlink
aidenybai Apr 27, 2026
9cf7b81
feat(cli): replace `watch` with `log` streaming command + file mirror
aidenybai Apr 28, 2026
4df3a22
fix(cli): two more bugbot findings on remove default scope + project …
aidenybai Apr 29, 2026
6740b66
feat(cli): standardize on .agents/skills for compatible agents
aidenybai Apr 29, 2026
78f1537
refactor(cli): hoist SKILL_SCOPES + isSkillScope to shared util
aidenybai Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ apps/
└── website/ # Documentation site (react-grab.com)

packages/
├── cli/ # CLI implementation (@react-grab/cli)
├── cli/ # CLI implementation (@react-grab/cli) including `react-grab log` and `install-skill`
├── grab/ # Bundled package (library + CLI, published as `grab`)
├── mcp/ # MCP server (@react-grab/mcp)
├── mcp/ # Deprecated stub for @react-grab/mcp (prints migration notice)
└── react-grab/ # Core library
```

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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.
It makes tools like Cursor, Claude Code, Copilot run up to [**2× faster**](https://benchmark.react-grab.com) and more accurate.

### [Try out a demo! →](https://react-grab.com)

Expand All @@ -19,18 +19,20 @@ Run this command at your project root (where `next.config.ts` or `vite.config.ts
npx grab@latest init
```

## Connect to MCP
## Install agent skill

```bash
npx grab@latest add mcp
npx grab@latest install-skill
```

Installs a `react-grab` skill into Cursor / Claude Code / Codex / OpenCode. Once installed, type `/react-grab` in your agent and click any element on the page — the agent receives the file name, React component, and HTML for that element.

## Usage

Once installed, hover over any UI element in your browser and press:

- **⌘C** (Cmd+C) on Mac
- **Ctrl+C** on Windows/Linux
- <kbd>⌘C</kbd> on Mac
- <kbd>Ctrl+C</kbd> 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:

Expand Down
72 changes: 53 additions & 19 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @react-grab/cli

Interactive CLI to install and configure React Grab in your project.
Interactive CLI to install and configure React Grab in your project, plus a `log` subcommand that streams React Grab clipboard payloads as NDJSON for AI coding agents.

## Quick Start

Expand All @@ -27,31 +27,54 @@ npx grab@latest init
| `--pkg <pkg>` | | Custom package URL |
| `--cwd <cwd>` | `-c` | Working directory (default: current dir) |

### `grab add`
### `grab install-skill`

Connect React Grab to your coding agent via MCP.
Install the `react-grab` skill into known agent skill directories (Cursor, Claude Code, Codex, OpenCode). Once installed, the agent will auto-invoke it on `/react-grab` or when the user references a previously-grabbed element.

```bash
npx grab@latest add mcp
npx grab@latest install-skill
```

| Option | Alias | Description |
| ------------- | ----- | ---------------------------------------- |
| `--yes` | `-y` | Skip confirmation prompts |
| `--cwd <cwd>` | `-c` | Working directory (default: current dir) |
| Option | Alias | Description |
| ------------------- | ----- | ------------------------------------------------------------- |
| `--yes` | `-y` | Install to all supported agents without prompting |
| `--agent <name...>` | `-a` | Install only to the named agent(s) (e.g. Cursor, Claude Code) |

For an interactive flow that first verifies React Grab is installed and offers a simpler project-vs-global choice, see `grab add`.

### `grab remove`

Disconnect React Grab from your coding agent.
Remove the React Grab skill from the selected agents.

```bash
npx grab@latest remove
```

| Option | Alias | Description |
| ------------------- | ----- | -------------------------------------------------- |
| `--yes` | `-y` | Remove from all supported agents without prompting |
| `--agent <name...>` | `-a` | Remove only from the named agent(s) |

### `grab log`

Stream every React Grab payload as NDJSON, one JSON object per line, until killed. The skill installed by `install-skill` shells out to this command — but you can also run it directly to script around grabs.

```bash
npx grab@latest remove mcp
npx -y @react-grab/cli log
```

| Option | Alias | Description |
| ------------- | ----- | ---------------------------------------- |
| `--yes` | `-y` | Skip confirmation prompts |
| `--cwd <cwd>` | `-c` | Working directory (default: current dir) |
Each line has the shape `{"prompt":"...","content":"..."}` (the `prompt` field is omitted when the user didn't type one in the toolbar). The command takes no flags. It always mirrors every line to `.react-grab/logs` (and writes a `.react-grab/.gitignore` so the log never lands in version control).

Lifecycle depends on stdout:

- **Interactive (TTY)**: streams forever, exits only on SIGINT/SIGTERM or a fundamental clipboard error (exit code `2`, e.g. SSH or missing `xclip`).
- **Piped or redirected (non-TTY)**: exits cleanly with code `0` after writing the first match. This is what makes `log | head -n 1` and `log > grabs.ndjson` terminate without manual intervention.

To grab a single payload from a script, pipe to `head`:

```bash
npx -y @react-grab/cli log | head -n 1
```

### `grab configure`

Expand Down Expand Up @@ -84,16 +107,27 @@ npx grab@latest init -y
# Set a custom activation key
npx grab@latest init -k "Meta+K"

# Connect MCP to your agent
npx grab@latest add mcp
# Install the React Grab skill into all supported agents
npx grab@latest install-skill -y

# Stream every grab as NDJSON until killed
npx -y @react-grab/cli log

# Take just the first grab and exit
npx -y @react-grab/cli log | head -n 1

# Change activation mode to hold
npx grab@latest configure --mode hold --hold-duration 500

# Interactive configuration wizard
npx grab@latest configure
```

## Migration from @react-grab/mcp

`@react-grab/mcp` is deprecated. To migrate:

1. Run `npx grab@latest install-skill`.
2. Remove the `react-grab-mcp` entry from your agent's `mcp.json` (Cursor, Claude Code, Codex, OpenCode, Windsurf, etc.).
3. Restart your agent. Type `/react-grab` and click an element.

## Supported Frameworks

| Framework | Detection |
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build": "rm -rf dist && NODE_ENV=production vp pack",
"test": "vp test run",
"test:watch": "vp test",
"typecheck": "tsc --noEmit",
"lint": "vp lint",
"format": "vp fmt",
"format:check": "vp fmt --check",
Expand All @@ -34,7 +35,8 @@
"ora": "^8.2.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"smol-toml": "^1.6.0"
"smol-toml": "^1.6.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/prompts": "^2.4.9"
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { Command } from "commander";
import { add } from "./commands/add.js";
import { checkInstalled } from "./commands/check-installed.js";
import { configure } from "./commands/configure.js";
import { init } from "./commands/init.js";
import { installSkill } from "./commands/install-skill.js";
import { log } from "./commands/log.js";
import { remove } from "./commands/remove.js";
import { upgrade } from "./commands/upgrade.js";
import { isTelemetryEnabled } from "./utils/is-telemetry-enabled.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&v=${VERSION}&t=${Date.now()}`).catch(() => {});
} catch {}
if (isTelemetryEnabled()) {
try {
fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
} catch {}
}

const program = new Command()
.name("grab")
Expand All @@ -25,6 +31,9 @@ program.addCommand(add);
program.addCommand(remove);
program.addCommand(configure);
program.addCommand(upgrade);
program.addCommand(installSkill);
program.addCommand(log);
program.addCommand(checkInstalled);

const main = async () => {
await program.parseAsync();
Expand Down
109 changes: 74 additions & 35 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { Command } from "commander";
import pc from "picocolors";
import { detectNonInteractive } from "../utils/is-non-interactive.js";
import { detectProject } from "../utils/detect.js";
import { detectProject, findNearestProjectRoot } from "../utils/detect.js";
import { handleError } from "../utils/handle-error.js";
import { highlighter } from "../utils/highlighter.js";
import { installMcpServers, promptMcpInstall } from "../utils/install-mcp.js";
import {
installDetectedOrAllSkills,
promptSkillInstall,
type SkillScope,
} from "../utils/install-skill.js";
import { logger } from "../utils/logger.js";
import { prompts } from "../utils/prompts.js";
import { spinner } from "../utils/spinner.js";

const VERSION = process.env.VERSION ?? "0.0.1";

export const add = new Command()
.name("add")
.alias("install")
.description("connect React Grab to your agent via MCP")
.argument("[agent]", "agent to connect (mcp)")
.description("connect React Grab to your agent by installing the skill")
.argument("[agent]", "legacy alias kept for backward compatibility (e.g. mcp, skill)")
.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;
// Walk up from the user-provided cwd to the nearest project root so
// running `grab add` inside a subdirectory still anchors detection and
// the skill install on the actual project root rather than the subdir.
const cwd = findNearestProjectRoot(opts.cwd);
const isNonInteractive = detectNonInteractive(opts.yes);

const preflightSpinner = spinner("Preflight checks.").start();
Expand All @@ -39,48 +47,79 @@ export const add = new Command()

preflightSpinner.succeed();

if (agentArg && agentArg !== "mcp") {
const VALID_AGENT_ARGS: readonly string[] = ["mcp", "skill"];
if (agentArg === "mcp") {
logger.break();
logger.warn(
`Legacy agent packages are deprecated. Use ${highlighter.info("mcp")} instead.`,
`${highlighter.info("@react-grab/mcp")} is deprecated. Installing the React Grab skill instead.`,
);
logger.log(`Run ${highlighter.info("grab install-skill")} directly going forward.`);
logger.break();
} else if (agentArg && !VALID_AGENT_ARGS.includes(agentArg)) {
logger.break();
logger.error(
`Unknown agent "${agentArg}". Valid values: ${VALID_AGENT_ARGS.join(", ")} (or omit the argument).`,
);
logger.log(`Run ${highlighter.info("grab add mcp")} to install the MCP server.`);
logger.break();
process.exit(1);
}

if (agentArg === "mcp" || isNonInteractive) {
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);
}
if (isNonInteractive) {
// Project-scope installs anchor on the resolved project root, not
// the original cwd, so a subdirectory invocation in a monorepo still
// lands the skill in the same dir the project's agents will read.
const results = installDetectedOrAllSkills("project", projectInfo.projectRoot);
const hasSuccess = results.some((result) => result.success);
if (!hasSuccess) {
logger.break();
logger.error("Failed to install React Grab skill.");
logger.break();
process.exit(1);
}
logger.break();
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
logger.log("Restart your agents to activate.");
logger.break();
} else {
const didInstall = await promptMcpInstall();
if (!didInstall) {
logger.break();
const { skillScope } = await prompts({
type: "select",
name: "skillScope",
message: "Where should the React Grab skill be installed?",
choices: [
{ title: "In this project (committed to repo)", value: "project" },
{ title: "Globally (per-user)", value: "global" },
],
initial: 0,
});

if (skillScope === undefined) {
logger.break();
process.exit(0);
process.exit(1);
}

const outcome = await promptSkillInstall(skillScope as SkillScope, projectInfo.projectRoot);
if (outcome === "failed") {
// Distinguish a real install failure (couldn't write to any agent
// skill dir) from a benign user cancellation. The previous boolean
// return collapsed both into exit 0 and silently swallowed
// permission errors that wrapper scripts / CI need to detect.
logger.break();
logger.error("React Grab skill install did not write any files.");
logger.break();
process.exit(1);
}
if (outcome === "cancelled") {
// Exit 1 on user-cancelled prompts so wrapper scripts can
// distinguish a cancellation from a successful install.
// Consistent with the scope-prompt cancellation branch above and
// with `grab install-skill`, which also exits 1 on multiselect
// cancel.
logger.break();
process.exit(1);
}
logger.break();
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
logger.log("Restart your agents to activate.");
logger.break();
}

logger.break();
logger.log(`${highlighter.success("Success!")} React Grab skill installed.`);
logger.log("Restart your agent(s) to pick it up.");
logger.break();
} catch (error) {
handleError(error);
}
Expand Down
Loading
Loading