Skip to content

feat(mcp): read element context from the OS clipboard#311

Open
aidenybai wants to merge 37 commits intomainfrom
permissionless-clipboard-mcp
Open

feat(mcp): read element context from the OS clipboard#311
aidenybai wants to merge 37 commits intomainfrom
permissionless-clipboard-mcp

Conversation

@aidenybai
Copy link
Copy Markdown
Owner

@aidenybai aidenybai commented Apr 25, 2026

Summary

  • The browser plugin no longer makes localhost network requests. The MCP server reads the existing application/x-react-grab MIME type directly off the OS clipboard.
  • Replaces the POST /context + /health channel with per-OS clipboard readers: osascript -l JavaScript (macOS), wl-paste/xclip (Linux), PowerShell -Sta + WinForms (Windows), and a WSL bridge that tries the Windows host via powershell.exe then falls back to WSLg via the Linux reader.
  • Adds a /skills/react-grab skill that tells agents to call the get_element_context MCP tool whenever the user references a grabbed element.
  • Drops the @react-grab/mcp/client export and the browser IIFE bundle. Drops the react-grab and fkill workspace deps from @react-grab/mcp. CLI is now always stdio.
  • @react-grab/cli no longer adds the now-pointless --stdio flag to generated MCP configs (existing configs keep working since the flag is silently ignored).

Why permissionless

  • No localhost requests from the browser side, so no CORS, no mixed-content warnings, no Chrome local-network-access prompts.
  • The clipboard already carries the payload in three formats (text/plain, text/html, application/x-react-grab JSON) — we just read the one that's already there.

Failure-mode handling

Environment Reader Hint surfaced when broken
macOS osascript -l JavaScript JXA "macOS requires osascript (preinstalled). Check $PATH."
Linux X11 xclip -selection clipboard -t application/x-react-grab -o "Install a custom-MIME clipboard reader: apt install xclip (X11) or apt install wl-clipboard (Wayland)."
Linux Wayland wl-paste -t application/x-react-grab -n, falls back to xclip same install hint
Windows powershell.exe -Sta + System.Windows.Forms.Clipboard::GetData "Cannot launch powershell.exe. Ensure Windows PowerShell is on PATH."
WSL host PowerShell first, then WSLg, then a WSL interop hint "Could not reach the Windows clipboard from WSL. Enable WSL interop (set enabled = true under [interop] in /etc/wsl.conf)..."
SSH bails immediately "Clipboard channel is unavailable in SSH sessions. Run react-grab-mcp on the same machine as your browser."

runExecFile attaches stdout/stderr to its rejection so timeouts, non-zero exits, and Add-Type failures all log the actual diagnostic via [react-grab-mcp] <bin> stderr: ....

Test plan

  • pnpm --filter @react-grab/mcp test — 49/49 passing
  • pnpm test:cli — 116/116 passing (the --stdio removal is reflected in install-mcp.test.ts)
  • pnpm lint — clean
  • pnpm typecheck — clean
  • pnpm format — applied
  • pnpm --filter @react-grab/mcp build — clean (no more client.* artifacts)
  • Live macOS end-to-end: wrote a real payload to NSPasteboard via JXA, ran the full MCP stdio handshake (initialize + tools/list + tools/call get_element_context), got the formatted prompt + element snippet back
  • Negative paths verified live: empty clipboard, expired payload (timestamp older than CONTEXT_TTL_MS), simulated SSH session via SSH_CLIENT env

Notes / breaking changes

  • @react-grab/mcp/client export is removed. Any external consumer importing the empty plugin needs to drop the import (the plugin was a no-op after the rewrite).
  • The HTTP /context and /health endpoints and the PORT env var are gone. react-grab-mcp is stdio-only now.
  • No changeset is included; pin the bump to major (or whichever your release cadence prefers) when releasing.

Note

Medium Risk
Moderate risk because it replaces agent-integration flow (MCP config install) with new skill install/remove commands and adds cross-platform clipboard readers/streaming behavior that can vary by OS/shell environment.

Overview
Switches agent integration from MCP server configuration to installing a react-grab agent skill (grab install-skill, updated grab add flow) and adds grab remove support for uninstalling skills with project/global scope handling and monorepo-aware root detection.

Adds a new grab log command that reads application/x-react-grab from the OS clipboard (macOS/Linux/Windows/WSL/SSH-aware), streams grabs as NDJSON (optionally exits after first match when piped), and mirrors output to .react-grab/logs with an auto-created .gitignore.

Introduces grab check-installed/is-installed, telemetry gating via CI/opt-out env vars, and replaces the old MCP installer code with a skill installer implementation (including persisted “last selected agents” state) plus extensive new unit tests and updated docs.

Reviewed by Cursor Bugbot for commit 78f1537. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Replaces the MCP server with a clipboard‑driven agent skill and a streaming CLI. react-grab log reads application/x-react-grab from the OS clipboard, emits NDJSON, mirrors to .react-grab/logs, and @react-grab/mcp is now a deprecation stub.

  • New Features

    • log: streams continuously; in piped mode exits after first match; writes .react-grab/logs (auto .gitignore); outputs lines as {"prompt"?,"content"}.
    • install-skill/remove/add: standardizes on .agents/skills/ for compatible agents; adds Windsurf and Pi as universal; Amp uses .agents/skills/ (project) and ~/.config/agents/skills/ (global); refuses unsupported clients (e.g. Cline); remembers last-selected agents and filters them against currently supported clients; walks up from subdirs to the workspace/project root; remove defaults to project scope.
    • check-installed (alias is-installed): exits 0/1; --json includes projectRoot and requestedCwd; walks up to the nearest project root; treats the source repo and monorepo workspaces as installed when applicable.
    • Telemetry: gated by DISABLE_TELEMETRY, DO_NOT_TRACK, and common CI envs.
    • Skill template: single-sourced markdown bundled in @react-grab/cli and symlinked at skills/react-grab/SKILL.md.
  • Bug Fixes

    • Clipboard readers: macOS decodes Chromium/WebKit web‑custom‑data; Windows handles System.IO.Stream and avoids UTF‑8 BOM; Linux Wayland only falls back to xclip on ENOENT; WSL bridges host PowerShell and WSLg and combines hints; SSH fast‑exit; emit helper stderr; fast‑exit when helper binary is missing.
    • Log loop: avoids stale grabs after an initial parse failure; exits cleanly in piped mode; flushes stderr before exit.
    • Installer/remover: preserves the shared canonical skill when other universal agents still use it; won’t persist auto‑routed selections; init -y installs (and re‑runs) the skill; add exits 1 on failed install and on cancel.
    • Formatting: dedupes the prompt and strips its untrimmed prefix while keeping the canonical elements body; threads extraPrompt to commentText for getContent payloads.

Written for commit 7d36e4e. Summary will update on new commits. Review in cubic

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-grab-storybook Ready Ready Preview, Comment May 1, 2026 1:28am
react-grab-website Ready Ready Preview, Comment May 1, 2026 1:28am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 25, 2026

Open in StackBlitz

npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/cli@311
npm i https://pkg.pr.new/aidenybai/react-grab/grab@311
npm i https://pkg.pr.new/aidenybai/react-grab/@react-grab/mcp@311
npm i https://pkg.pr.new/aidenybai/react-grab@311

commit: 78f1537

Comment thread packages/cli/src/utils/read-clipboard-windows.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 33 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/mcp/src/utils/read-clipboard-windows.ts">

<violation number="1" location="packages/mcp/src/utils/read-clipboard-windows.ts:11">
P1: Reading the clipboard by `application/x-react-grab` will miss Chromium's custom clipboard format on Windows, so browser-copied element context is likely always unavailable there.</violation>

<violation number="2" location="packages/mcp/src/utils/read-clipboard-windows.ts:18">
P1: The fallback `$data.ToString()` in the `else` branch will silently produce `"System.IO.MemoryStream"` instead of the actual payload content. For custom clipboard formats, `[System.Windows.Forms.Clipboard]::GetData()` typically returns a `System.IO.MemoryStream`, not a `byte[]` or a string. Add a `System.IO.Stream` branch that reads the stream content as UTF-8:

```powershell
} elseif ($data -is [System.IO.Stream]) {
    $reader = [System.IO.StreamReader]::new($data, [System.Text.Encoding]::UTF8)
    [Console]::Out.Write($reader.ReadToEnd())
    $reader.Close()
```</violation>
</file>

<file name="packages/mcp/src/utils/read-clipboard-wsl.ts">

<violation number="1" location="packages/mcp/src/utils/read-clipboard-wsl.ts:15">
P2: Preserve the Linux fallback hint instead of always overwriting it with the WSL interop message.</violation>
</file>

<file name="packages/mcp/src/utils/read-clipboard-linux.ts">

<violation number="1" location="packages/mcp/src/utils/read-clipboard-linux.ts:45">
P2: A non-ENOENT `wl-paste` failure returns early instead of falling back to `xclip`, so clipboard reads can fail on Wayland/XWayland setups where the X11 reader would still work.</violation>
</file>

<file name="packages/mcp/src/server.ts">

<violation number="1" location="packages/mcp/src/server.ts:22">
P2: `payload.content` can already include the user prompt, so this formats prompted grabs with the prompt duplicated in both `Prompt:` and `Elements:`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/cli/src/utils/read-clipboard-windows.ts
Comment thread packages/cli/src/utils/read-clipboard-windows.ts
Comment thread packages/mcp/src/utils/read-clipboard-wsl.ts Outdated
Comment thread packages/mcp/src/utils/read-clipboard-linux.ts
Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/server.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/mcp/src/server.ts">

<violation number="1" location="packages/mcp/src/server.ts:30">
P2: Using `entry.content` to rebuild the elements body loses the canonical formatting in `payload.content`. Multi-element copies will drop their `[1]`/`[2]` labels, and any `transformCopyContent` output is discarded.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/utils/read-clipboard-linux.ts
Comment thread packages/cli/src/utils/read-clipboard-windows.ts
Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/server.ts Outdated
Comment thread packages/mcp/src/server.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/mcp/src/server.ts">

<violation number="1" location="packages/mcp/src/server.ts:27">
P2: The trimmed prompt fallback can truncate real element content that happens to start with the prompt text.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/mcp/src/server.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found across 66 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/commands/watch.ts">

<violation number="1" location="packages/cli/src/commands/watch.ts:74">
P1: Let the process exit naturally after writing the payload; `process.exit(0)` here can truncate stdout.</violation>
</file>

<file name="skills/react-grab/SKILL.md">

<violation number="1" location="skills/react-grab/SKILL.md:19">
P2: Don't route pasted React Grab output through `watch`; it waits for a newer clipboard timestamp and will hang on already-pasted context.</violation>
</file>

<file name="packages/cli/src/utils/install-skill.ts">

<violation number="1" location="packages/cli/src/utils/install-skill.ts:137">
P2: Unsupported agents are exposed as valid install/remove targets, so commands can accept VS Code or Zed and then no-op as if the selection were valid.</violation>
</file>

<file name="packages/mcp/src/cli.ts">

<violation number="1" location="packages/mcp/src/cli.ts:15">
P3: Avoid `process.exit(1)` here; it can cut off the deprecation notice before stderr flushes.</violation>
</file>

<file name="packages/cli/src/utils/wait-for-next-grab.ts">

<violation number="1" location="packages/cli/src/utils/wait-for-next-grab.ts:53">
P2: The polling loop does not enforce the requested timeout while `read()` is in flight, so short `watch --timeout` values can still block for the clipboard reader's full 3s timeout.</violation>
</file>

<file name="packages/cli/src/commands/init.ts">

<violation number="1" location="packages/cli/src/commands/init.ts:350">
P1: Project-scoped skill installs use the original cwd, so selecting a subproject can write the skill into the wrong repo directory.</violation>
</file>

<file name="packages/cli/test/watch-cli.test.ts">

<violation number="1" location="packages/cli/test/watch-cli.test.ts:6">
P1: Use an ESM-safe path source here; `__dirname` is undefined in this package.</violation>
</file>

<file name="packages/cli/README.md">

<violation number="1" location="packages/cli/README.md:43">
P3: `grab add` is not an alias of `install-skill`; it is a separate wrapper command with different behavior. Reword this to avoid implying the commands are interchangeable.</violation>
</file>

<file name="packages/cli/src/utils/last-selected-agents.ts">

<violation number="1" location="packages/cli/src/utils/last-selected-agents.ts:12">
P2: Ignore relative `XDG_STATE_HOME` values before building the state path.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:9">
P1: Keep `@react-grab/mcp` in the root build filter so its `dist` binary is generated before publishing.</violation>
</file>

<file name="packages/cli/src/commands/install-skill.ts">

<violation number="1" location="packages/cli/src/commands/install-skill.ts:85">
P2: Check `installSkills()` results before printing the restart/success message. This command currently reports success even when every requested install was skipped or failed.</violation>
</file>

<file name="packages/cli/src/utils/format-payload.ts">

<violation number="1" location="packages/cli/src/utils/format-payload.ts:27">
P2: Prompt-mode payloads created through `getContent` lose their `Prompt:` section because this formatter only reads prompts from `entries.commentText`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/cli/src/commands/watch.ts Outdated
Comment thread packages/cli/src/commands/init.ts Outdated
Comment thread packages/cli/test/watch-cli.test.ts Outdated
Comment thread package.json Outdated
Comment thread skills/react-grab/SKILL.md Outdated
Comment thread packages/cli/src/utils/last-selected-agents.ts Outdated
Comment thread packages/cli/src/commands/install-skill.ts Outdated
Comment thread packages/cli/src/utils/format-payload.ts Outdated
Comment thread packages/mcp/src/cli.ts Outdated
Comment thread packages/cli/README.md Outdated
aidenybai and others added 30 commits April 30, 2026 18:27
…aller

Reduces @react-grab/mcp to a 0.2.0 deprecation stub and folds the clipboard
reader into @react-grab/cli as a `react-grab watch` subcommand that polls
the system clipboard's `application/x-react-grab` payload and exits when a
fresh grab arrives (default 10-min timeout, `--timeout 0` blocks forever).
SSH/WSL "unrecoverable" envs now fast-exit with code 2 instead of polling
out the timeout.

Adds `react-grab install-skill` that writes SKILL.md to known agent skill
directories. Universal agents (Cursor, Codex, OpenCode, Amp, Cline, Gemini
CLI, GitHub Copilot, Warp) collapse to a single canonical write at
.agents/skills/ (project) or ~/.agents/skills/ (global), matching the
vercel-labs/skills convention. Non-universal agents (Claude Code,
Windsurf, Droid) get their own paths.

Auto-detects installed agents via existsSync against telltale dirs and
remembers the last-selected agents under
${XDG_STATE_HOME ?? ~/.local/state}/react-grab/. Honors CLAUDE_CONFIG_DIR,
CODEX_HOME, XDG_CONFIG_HOME. Telemetry pings now skip under
DISABLE_TELEMETRY / DO_NOT_TRACK and in common CI environments.

Skill template ships `allowed-tools: [Bash]` frontmatter per the Agent
Skills Specification.

Adds parse-timeout-seconds, wait-for-next-grab, format-payload,
is-telemetry-enabled, last-selected-agents, skill-template utilities plus
229 unit tests (clipboard readers, watch outcomes, install/remove dedup,
SSH fast-exit, deprecation stub).
- watch: don't `process.exit(0)` after writing the payload — return so Node
  drains stdout naturally (avoids truncation when piped).
- init: pass `projectInfo.projectRoot` (not the original `cwd`) to the skill
  installer so subprojects in monorepos get the skill in the right dir.
- root: re-add `--filter=@react-grab/mcp` to the build script so the
  deprecation stub `dist` artifact is generated before publish.
- install-skill / remove: reject `--agent` arguments that name unsupported
  clients (VS Code, Zed) instead of silently no-opping.
- skill template: clarify the trigger — agents should NOT run `watch` if the
  user has already pasted React Grab toolbar output (it would block waiting
  for a fresh clipboard timestamp that is not coming).
- last-selected-agents: ignore relative `$XDG_STATE_HOME` values per the
  XDG Base Directory spec; fall back to `~/.local/state`.
- subprocess tests (watch-cli, deprecation-stub): use
  `fileURLToPath(import.meta.url)` instead of `__dirname` so they're
  ESM-pure.
- install-skill: filter unsupported clients out of `--yes` fallback,
  multiselect prompt, and only print "Restart your agent(s)..." when at least
  one install actually succeeded. Mirror the result-checking in the
  single-detected and explicit-agent branches too.
- clipboard readers: when the OS helper binary is missing (osascript /
  xclip+wl-paste / powershell), set `recoverable: false` so `watch` fast-exits
  with code 2 and the install hint, instead of polling out the timeout.
- mcp deprecation stub: replace `process.exit(1)` with `process.exitCode = 1`
  so the deprecation notice fully flushes before exit.
- cli README: clarify that `grab add` is a wrapper around `install-skill`,
  not a strict alias.

Skipping two pre-existing/edge-case items: format-payload prompt-mode
(producer always uses entries[].commentText, schema has no top-level prompt)
and wait-for-next-grab read-blocks-timeout (refactor cost > 0.5% overshoot
benefit at the default 600s timeout).
…uccess

- WSL reader: stay recoverable as long as either the Windows host or the
  WSLg Linux channel can still produce. Previously, when one channel
  returned ENOENT the combined outcome was marked unrecoverable even when
  the other channel could still serve a valid grab once one appeared.
- install-skill: only call writeLastSelectedAgents AFTER confirming at
  least one install succeeded. Previously the --agent and --yes branches
  persisted the selection unconditionally, so a failed run biased future
  interactive multiselect pre-checks.

New WSL test confirms the recoverability composition: both-channels-failed
=> unrecoverable; one-channel-failed-but-other-empty => still recoverable.
…stall

Same fix as init.ts (subprojects in monorepos). The non-interactive
`installDetectedOrAllSkills` and the interactive `promptSkillInstall` paths
both now anchor on the resolved project root rather than the launch cwd, so
agents find the installed skill in the dir they're scanning.
bugbot flagged that fail() writes to stderr then immediately calls
process.exit, which can truncate output on piped consumers (every agent
tool harness pipes stderr). The match path already returned naturally;
fix the same pattern for unrecoverable/timeout/aborted/default by:

- Using process.stderr.write(msg, callback) and putting process.exit
  inside the callback - the callback only fires once the kernel buffer
  has accepted the write.
- Throwing an ExitSignal sentinel to halt synchronous execution; the
  action's outer try/catch swallows it so the user sees only the message
  we just wrote.
bugbot: when init() runs in a fresh project, an optional skill install
write failure was calling process.exit(0) before the main React Grab
install/transform happened. Treat skill install as decoration: warn on
failure and continue to install React Grab itself.
- Linux Wayland reader: only fall through to xclip when wl-paste binary
  is missing (ENOENT). Previously any non-zero exit fell through, which
  on Wayland-only systems with the common 'no data of that mime' case
  surfaced a misleading 'install xclip' hint. Update the test to assert
  the new behavior: runtime wl-paste failure returns empty payload, not
  an xclip retry.
- init.ts already-installed branch: when the user opts into a skill
  install and it fails, surface the failure and exit 1 (was silent
  exit 0). Mirrors the warn-and-continue pattern in the fresh-install
  branch but with non-zero exit since skill install was the only action
  on this path.
…ally finds the payload

Chromium and WebKit on macOS do NOT expose web-custom-format MIME types
(anything the page wrote via clipboardData.setData(type, data) for a
non-standard MIME) under the raw type name on NSPasteboard. They bundle
all such entries into a single pasteboard type:

  org.chromium.web-custom-data
  org.webkit.web-custom-data

containing a base::Pickle with [count, then for each: mime length in
UTF-16 code units, mime UTF-16 LE bytes, padded to 4 bytes, value
length in UTF-16 code units, value UTF-16 LE bytes, padded].

Our previous JXA called dataForType('application/x-react-grab')
directly and got nil for browser-written grabs, so watch polled forever
without ever seeing the payload. Confirmed live: a Cursor session's
clipboard exposed `org.chromium.web-custom-data` (with vscode-editor-data
inside) and our raw lookup returned nil.

Fix:
- New util `decode-chromium-web-custom-data.ts` parses the pickle
  format with proper 4-byte alignment.
- macos JXA now tries direct first (covers Safari/Firefox direct
  exposure and any future browser change), then falls back to
  org.chromium.web-custom-data, then org.webkit.web-custom-data,
  emitting a sentinel-prefixed base64 dump that the Node side decodes.

Tests:
- 6 unit tests for the pickle decoder (single entry, scan past
  unrelated, alignment padding, truncation handling, missing target).
- macos reader tests now assert the JXA script references all three
  pasteboard types and decodes the sentinel-prefixed pickle correctly.

Verified end-to-end live: wrote a fake pickle to NSPasteboard, ran
`watch --timeout 8 --json`, swapped in a fresh-timestamp pickle during
polling, watch exited 0 with the correct JSON on stdout.
- Move PICKLE_SENTINEL + PICKLE_ALIGNMENT + MAX_ENTRIES to constants.ts
  (per AGENTS.md rule on shared constants).
- Harden the JXA->Node sentinel to '\u0001CHROMIUM_PICKLE_B64\u0002' so
  it can never collide with a direct-path payload (valid JSON cannot
  start with control bytes; parseReactGrabPayload validates JSON shape
  downstream regardless).
- Cap entryCount at 1024 to keep a malicious / corrupt pickle from
  looping for a long time. Pre-existing buffer-bound checks already
  bounded total work, but the explicit cap is defensive.
- Decoder + macOS reader headers now mention WebKit and document that
  the WebKit pickle layout was not verified empirically; the parser
  reuses the Chromium logic on a best-effort basis and returns null
  cleanly if the format differs.
- Tests:
  - Import sentinel + alignment from constants.ts (was duplicated as
    string literals in two test files).
  - Add 'declared payload size larger than buffer' test.
  - Add 'entry count above MAX' test.
  - Add 'entry count exactly at MAX' test.
- remove command refuses to delete a shared canonical file when other
  un-targeted universal agents still use it. `grab remove --agent Cursor`
  used to wipe `.agents/skills/react-grab/SKILL.md` and break the skill
  for every other universal agent (Codex, OpenCode, Amp, Cline, Gemini
  CLI, GitHub Copilot, Warp). Now it's preserved with a clear "kept:
  still used by ..." message and a hint to pass --agent for every
  sharer if the user really wants to remove.
- install-skill --yes wholesale fallback (no detected agents) no longer
  persists the resulting "every supported agent" list as last-selected.
  Persistence is now restricted to explicit signals: detected agents,
  --agent flag, single-detected auto-install, interactive multiselect.
  Future interactive runs no longer get pre-checked with ~12 agents the
  user never deliberately chose.

3 new tests:
- removeSkills: full canonical wipe when ALL universal agents targeted
- removeSkills: refuses + reports sharedWith when subset targeted
- removeSkills: non-universal agent removed without touching shared file
Per bugbot finding: `grab remove` invoked from a subdirectory in a
monorepo silently no-ops while the actual SKILL.md sits at
`<projectRoot>/.agents/skills/react-grab/`. Same blind spot affects
`grab install-skill` (writes a stray `.agents/skills` under the subdir
that agents won't pick up) and `grab add` (detectProject returns the
input cwd as projectRoot without walking up).

- Add `findNearestProjectRoot(start)` in detect.ts that walks up
  looking for the nearest `package.json`, capped at 64 levels, falling
  back to `start` if nothing is found before the filesystem root.
- `install-skill`, `remove`, `add` all run cwd through it before
  passing to skill installer / remover / detectProject.
- New tests: 4 cases for the walker (self, walk up, deepest workspace
  package wins, fallback when no package.json).
- Smoke verified end-to-end: install from
  `$TMP/packages/web/src/components` → writes to
  `$TMP/.claude/skills/react-grab/SKILL.md`; remove from the same
  subdir → finds and deletes that exact file.
- monorepo workspace root: findNearestProjectRoot now prefers the
  outermost ancestor that's a workspace root (pnpm-workspace.yaml,
  lerna.json, or package.json with non-empty `workspaces` field) over
  the deepest plain package.json. Without this, install-skill / remove
  / add invoked from `<repo>/packages/web/src` resolved to
  `<repo>/packages/web` and the canonical `.agents/skills/...` ended up
  under the workspace package, not the repo root where editor agents
  actually look.
- init -y now installs the React Grab skill (default to project scope)
  to match the pre-CLI MCP behavior. Without this fix, scripted
  `npx grab init -y` pipelines silently lose agent integration after
  upgrading.
- installSkills spinner reports failure when every selected client was
  skipped (e.g. only unsupported agents like Zed/VS Code). The previous
  green "Installed to 0 agents." check contradicted the per-client
  "skipped" lines and the eventual non-zero exit.
- readClipboardWsl falls through to WSLg when the Windows host returns
  a non-JSON-shaped payload. Previously a single garbage host read
  would cause the watch loop to idle until timeout even when WSLg held
  a valid grab.

Tests: 5 new walker tests (workspace markers, npm/yarn workspaces,
lerna, plain repo fallback, no-project fallback). 2 new WSL tests
(garbage-host fallthrough, surface garbage when no channel has JSON).
…emplate

- New `grab check-installed` (also `grab is-installed`) command. Exits
  0 if react-grab is in the project's package.json dependencies,
  exits 1 otherwise. Supports `--json` for scripted usage. Useful for
  CI / automation pipelines that want to gate further setup on
  whether react-grab is already installed.
- Tighten the SKILL.md description so agents pick the skill up on
  shorter, more natural references like "this thing" / "that
  component" / "/react-grab", while still keeping the explicit
  "don't run watch if content is already pasted" guidance.
promptSkillInstall returned a single boolean for both "user cancelled"
and "every install failed", and add.ts collapsed both into exit 0. A
genuine install failure (permission errors writing to an agent skill
dir, etc.) terminated with a success exit code while skipping the
"Success!" message - silently swallowed by wrapper scripts and CI.

Change return type to a discriminated SkillInstallOutcome union
("cancelled" | "succeeded" | "failed") and have add.ts exit 1 on
failed and 0 on cancelled, matching install-skill's exit semantics.
`grab check-installed` resolved cwd directly and called detectReactGrab
against that single directory, only reading `<cwd>/package.json`.
Sibling commands (add, install-skill, remove) walk up via
findNearestProjectRoot for exactly this reason.

The skill template instructs the agent to run `check-installed` as the
preflight before `watch`, so when the agent's working directory is any
subdirectory of the project (common in monorepos and even in single-
package projects when the agent opens a deeper folder), the preflight
falsely reported react-grab as not installed and the skill prompted
the user to run `grab init` despite it already being set up.

New test: invoke from `<workDir>/packages/ui/src` and verify the JSON
output reports the project root and `installed: true`.
…ME accuracy

- copy.ts: when callers use the getContent override (no per-element
  snippets), entries was left undefined and copy-content.ts built a
  default entry with commentText: undefined. Downstream formatters then
  dropped the "Prompt:" section even though extraPrompt had been prepended
  to payload.content. Pass extraPrompt as the top-level commentText so
  the default entry surfaces the prompt.
- cli/README: clarify that `grab add` is a separate higher-level command
  (preflights React Grab installation, simpler scope choice) rather than
  a wrapper/alias of install-skill.
`detectReactGrab` only inspected the project root, which broke two
common cases:

1. Working inside the react-grab source monorepo itself - the root
   package.json has no react-grab dep because react-grab IS the package
   defined under packages/react-grab. The skill preflight then falsely
   prompted users to run `grab init` on the source repo.
2. Consumer monorepos where only one or two app packages depend on
   react-grab - the root package.json carries no react-grab dep, so the
   preflight misfired anywhere outside that one app.

Now also: (a) treat a package whose own `name` is `react-grab` as
installed, and (b) walk every workspace package and re-check via the
single-package detector before giving up.
- Linux ENOENT detection (Medium): isBinaryMissing now relies solely on
  error.code === 'ENOENT'. The previous /not found/i regex matched
  wl-paste's runtime "No data found of type X" stderr (the common
  "MIME isn't on the clipboard right now" case), which incorrectly
  routed to the X11 fallback on Wayland and surfaced the misleading
  "install xclip" hint as unrecoverable.
- watch fail() reachability (Medium): all fail() call sites now use
  `return fail(...)` instead of expression-statements so TS control-
  flow analysis treats every branch as terminating, even if fail is
  ever refactored away from throwing synchronously.
- removeSkills false sharedWith (Low): existence check before deciding
  between "removed", "shared with another agent", and "nothing here".
  `grab remove --agent Cursor` against a project that never installed
  the skill no longer prints "kept: still used by Codex, OpenCode, ..."
  for a file that doesn't exist; falls through to "Nothing to remove."
- hasWorkspacesField alignment (Low): mirror detectMonorepo's permissive
  "any truthy `workspaces` counts" rule so findNearestProjectRoot can
  never disagree with detectMonorepo on the same package.json.
- install-skill --yes single-detected persistence (Low): don't persist
  the auto-routed agent as "last selected" when the user didn't make
  an active choice. Prevents future interactive runs from being
  silently restricted to a single agent.
- install-skill cancellation exit code (Low): exit 1 on user-cancelled
  multiselect to match the scope-prompt cancellation branch and `add`'s
  exit semantics, so wrapper scripts can distinguish a cancelled
  install from a successful one.

NOT fixed (bugbot wrong on JXA):
- read-clipboard-macos selector convention: bugbot claimed JXA needs
  underscore-per-colon (initWithData_encoding_, base64EncodedStringWithOptions_),
  but empirically those raise "is not a function" on current macOS.
  Reverted to camelCase forms (verified live: writes a fresh pickle to
  org.chromium.web-custom-data, watch reads + decodes + exits 0 with
  the parsed JSON payload). Added comment documenting the empirical
  behavior so it doesn't get "fixed" again.

5 new tests; total 263/263 passing.
…nts; rename JSON field

- check-installed: use the write-callback flush pattern from watch.ts so
  stdout/stderr drain before process.exit (avoids truncation when piped
  through agent tool harnesses).
- check-installed: wrap action body in try/catch + handleError to match
  every other command's error reporting.
- check-installed: rename JSON output field cwd -> projectRoot and add
  requestedCwd so consumers can see both the input and the resolved
  walk-up target unambiguously.
- detect.ts: add unit tests for the two new detectReactGrab branches
  (name === "react-grab" short-circuit, monorepo workspace walk).
- detect.ts, copy.ts, tests: trim multi-line comments per AGENTS.md
  ("default to no comments; only when the why is non-obvious").
- init -y now installs the React Grab skill on re-runs against an
  already-installed project (Medium). The previous fix only added skill
  install to the fresh-install non-interactive branch, so CI scripts
  that re-ran `npx grab init -y` after a successful first install
  silently lost agent integration.
- watch no longer returns a stale grab when the initial clipboard
  read transiently fails to parse (Medium). Threaded a new
  `rawPayloadPresent` signal through readClipboardPayload ->
  waitForNextGrab so the polling loop can distinguish "clipboard was
  genuinely empty at start" (first non-null = match) from "parse
  failed on a real but corrupt/partial read" (first non-null = new
  baseline, wait for the next change). 2 new tests cover both arms of
  the discriminated logic.
- promptSkillInstall single-detected auto-route no longer persists
  the chosen agent as last-selected (Medium). Symmetrical to the
  install-skill fix from the previous round - the user didn't make
  an active choice when we auto-routed, so persisting would
  silently bias every future interactive multiselect to that single
  agent and suppress the auto-route on subsequent runs.

268/268 tests passing.
- add cancellation now exits 1 (Low). Brings `grab add`'s
  cancellation exit code in line with `grab install-skill` and the
  scope-prompt branch. Wrapper scripts can now reliably distinguish
  a user-aborted multiselect from a successful install across both
  commands.
- WSL combined hint now retains the host-side guidance (Low).
  Previously combineHints(WSL_INTEROP_HINT, wslgOutcome.hint) silently
  dropped hostOutcome.hint, hiding the most actionable error when the
  real failure was e.g. PowerShell missing rather than disabled
  interop. Now host hint -> WSL_INTEROP_HINT -> WSLg hint, in that
  order. Test updated to assert the host-specific marker is present.
Move the canonical skill content out of `skill-template.ts`'s string literal
into a real `skill-template.md` next to it. The bundler inlines its contents
at build time via a `define` substitution, and the repo-root
`skills/react-grab/SKILL.md` is now a symlink to the same file - so the
GitHub-visible copy and the bundled copy can never drift apart.
`grab log` emits every React Grab payload as NDJSON
(`{prompt?, content}` per line) and mirrors each line to
`.react-grab/logs` (auto-gitignored via `.react-grab/.gitignore`).
TTY users get continuous streaming; piped mode (`log | head -n 1`)
exits cleanly after the first match so agent skills don't wait
on a still-running pipeline.

Drops the 10-minute idle timeout - log no longer voluntarily
exits except on a fundamental clipboard-read error (SSH, missing
helper). Skill template, README, mcp migration table, and docs
updated to match.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…root resolve

- `grab remove` no longer wipes the user's global skill by default. Without
  an explicit `--scope`, only project scope is touched. The interactive
  multiselect only asks WHICH agents - never which scope - so a user
  cleaning up a single project would silently lose their global install
  across every other project on the machine. Pass `--scope global` to
  remove the per-user copy.
- `findNearestProjectRoot` returns the resolved absolute path on the
  no-package.json fallback branch. Every other branch already returned an
  absolute path; the relative-`--cwd` leak only fired here.

282 tests passing (+1).
Re-verified each agent's actual support for the Agent Skills `.agents/skills/`
convention (the open standard at agentskills.io) and updated the install map
accordingly:

- Windsurf: now universal. Cascade scans `.agents/skills/` and
  `~/.agents/skills/` per the official Windsurf docs, so installs dedup
  against the canonical location instead of writing a duplicate file under
  `.windsurf/skills/`.
- Pi (`badlogic/pi-mono`): added as universal. Detected via `~/.pi/`,
  scans `.agents/skills/` and `~/.agents/skills/` per the pi-mono docs.
- Amp: split paths. Project scope is canonical (`.agents/skills/`) so it
  dedups with other universal agents, but global scope writes to
  `~/.config/agents/skills/` because that's what Amp actually reads -
  previously the global install silently went to a directory Amp doesn't
  scan.
- Cline: demoted to unsupportedClient. Cline only reads from
  `.cline/skills/`, never `.agents/skills/`, so installing was a no-op.
  Surfaces a migration message instead of silently writing to the wrong
  path. `--agent Cline` still gets a clear "do not support skills yet"
  error rather than the misleading "unknown agent" path.

Also fixes a stale-state bug found during review:

- `readKnownLastSelectedAgents` filters the persisted last-selected list
  against the current client roster before consumption. Without this, a
  `["Cline"]` entry from a previous CLI version would skew the
  `lastSelected.length === 0` short-circuits used by the install flow and
  keep the multiselect's "user has a saved choice" branch active even
  when none of the saved choices map to a real agent anymore.

282 tests passing (+8).
Bugbot Low-severity DRY: `SKILL_SCOPES` and `isSkillScope` were defined
identically in both `commands/install-skill.ts` and `commands/remove.ts`.
Both files already import `SkillScope` from `utils/install-skill.ts`, so
co-locate the scope list and guard there to remove the maintenance risk
of updating one copy but not the other.

Pure refactor, no behavior change. 282 tests still passing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant