@react-grab/cli 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +555 -346
- package/dist/cli.js +558 -345
- package/dist/cli.js.map +1 -1
- package/dist/read-clipboard.ps1 +111 -0
- package/dist/read-clipboard.swift +35 -0
- package/package.json +7 -4
- package/skills/react-grab/SKILL.md +49 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Windows clipboard reader for `react-grab watch`. Reads CF_UNICODETEXT and the
|
|
2
|
+
# registered "Chromium Web Custom MIME Data Format" (a base::Pickle of web custom
|
|
3
|
+
# data) via Win32, in a single OpenClipboard so the two reads are consistent.
|
|
4
|
+
# Emits { changeCount, text, pickleBase64 } as JSON; the CLI decodes the pickle
|
|
5
|
+
# (shared with macOS/Linux). GetClipboardSequenceNumber gives a cheap monotonic
|
|
6
|
+
# change token for idle polling.
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
|
|
10
|
+
$source = @"
|
|
11
|
+
using System;
|
|
12
|
+
using System.Runtime.InteropServices;
|
|
13
|
+
|
|
14
|
+
public static class RgClip {
|
|
15
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
16
|
+
public static extern bool OpenClipboard(IntPtr hWndNewOwner);
|
|
17
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
18
|
+
public static extern bool CloseClipboard();
|
|
19
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
20
|
+
public static extern IntPtr GetClipboardData(uint uFormat);
|
|
21
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
22
|
+
public static extern uint RegisterClipboardFormat(string lpszFormat);
|
|
23
|
+
[DllImport("user32.dll")]
|
|
24
|
+
public static extern uint GetClipboardSequenceNumber();
|
|
25
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
26
|
+
public static extern bool IsClipboardFormatAvailable(uint format);
|
|
27
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
28
|
+
public static extern IntPtr GlobalLock(IntPtr hMem);
|
|
29
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
30
|
+
public static extern bool GlobalUnlock(IntPtr hMem);
|
|
31
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
32
|
+
public static extern UIntPtr GlobalSize(IntPtr hMem);
|
|
33
|
+
|
|
34
|
+
private const uint CF_UNICODETEXT = 13;
|
|
35
|
+
|
|
36
|
+
private static byte[] ReadLocked(uint format) {
|
|
37
|
+
if (!IsClipboardFormatAvailable(format)) return null;
|
|
38
|
+
IntPtr handle = GetClipboardData(format);
|
|
39
|
+
if (handle == IntPtr.Zero) return null;
|
|
40
|
+
IntPtr pointer = GlobalLock(handle);
|
|
41
|
+
if (pointer == IntPtr.Zero) return null;
|
|
42
|
+
try {
|
|
43
|
+
ulong size = GlobalSize(handle).ToUInt64();
|
|
44
|
+
if (size == 0 || size > int.MaxValue) return null;
|
|
45
|
+
byte[] bytes = new byte[(int)size];
|
|
46
|
+
Marshal.Copy(pointer, bytes, 0, (int)size);
|
|
47
|
+
return bytes;
|
|
48
|
+
} finally {
|
|
49
|
+
GlobalUnlock(handle);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static byte[][] ReadAll(uint customFormat) {
|
|
54
|
+
byte[][] result = new byte[2][];
|
|
55
|
+
if (!OpenClipboard(IntPtr.Zero)) return result;
|
|
56
|
+
try {
|
|
57
|
+
result[0] = ReadLocked(CF_UNICODETEXT);
|
|
58
|
+
result[1] = ReadLocked(customFormat);
|
|
59
|
+
} finally {
|
|
60
|
+
CloseClipboard();
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
"@
|
|
66
|
+
|
|
67
|
+
# Add-Type recompiles on every process start, so the reader is compiled to a
|
|
68
|
+
# cached assembly once and merely loaded on subsequent polls.
|
|
69
|
+
$cacheDir = Join-Path $env:TEMP "react-grab-watch"
|
|
70
|
+
# Key the cached DLL by a hash of the source so a changed reader recompiles
|
|
71
|
+
# instead of loading a stale assembly.
|
|
72
|
+
$sourceHashBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($source))
|
|
73
|
+
$sourceHash = ([System.BitConverter]::ToString($sourceHashBytes) -replace "-", "").Substring(0, 16)
|
|
74
|
+
$cachedDll = Join-Path $cacheDir "RgClipReader-$sourceHash.dll"
|
|
75
|
+
$loaded = $false
|
|
76
|
+
if (Test-Path $cachedDll) {
|
|
77
|
+
try { Add-Type -Path $cachedDll | Out-Null; $loaded = $true } catch {}
|
|
78
|
+
}
|
|
79
|
+
if (-not $loaded) {
|
|
80
|
+
try {
|
|
81
|
+
New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
|
|
82
|
+
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $cachedDll -ErrorAction Stop | Out-Null
|
|
83
|
+
Add-Type -Path $cachedDll | Out-Null
|
|
84
|
+
$loaded = $true
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
if (-not $loaded) {
|
|
88
|
+
Add-Type -TypeDefinition $source -Language CSharp | Out-Null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$changeCount = [RgClip]::GetClipboardSequenceNumber()
|
|
92
|
+
$customFormat = [RgClip]::RegisterClipboardFormat("Chromium Web Custom MIME Data Format")
|
|
93
|
+
$blobs = [RgClip]::ReadAll($customFormat)
|
|
94
|
+
|
|
95
|
+
$text = $null
|
|
96
|
+
if ($null -ne $blobs[0]) {
|
|
97
|
+
$text = [System.Text.Encoding]::Unicode.GetString($blobs[0]).TrimEnd([char]0)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$pickleBase64 = $null
|
|
101
|
+
if ($null -ne $blobs[1]) {
|
|
102
|
+
$pickleBase64 = [System.Convert]::ToBase64String($blobs[1])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$payload = [ordered]@{
|
|
106
|
+
changeCount = [int64]$changeCount
|
|
107
|
+
text = $text
|
|
108
|
+
pickleBase64 = $pickleBase64
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
$payload | ConvertTo-Json -Compress
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
// macOS clipboard reader for `react-grab watch`. React Grab's custom MIME type
|
|
4
|
+
// is not exposed directly by Chromium-based browsers: the legacy
|
|
5
|
+
// execCommand("copy") + dataTransfer.setData path lands as a base::Pickle under
|
|
6
|
+
// "org.chromium.web-custom-data"; the async Clipboard API path lands as raw
|
|
7
|
+
// bytes referenced by "org.w3.web-custom-format.map". This reader emits the
|
|
8
|
+
// pickle as base64 (the CLI decodes it, shared with Linux/Windows) and the W3C
|
|
9
|
+
// payload as a resolved string, plus changeCount for cheap idle polling.
|
|
10
|
+
|
|
11
|
+
let GRAB_MIME = "application/x-react-grab"
|
|
12
|
+
let pasteboard = NSPasteboard.general
|
|
13
|
+
var result: [String: Any] = ["changeCount": pasteboard.changeCount]
|
|
14
|
+
|
|
15
|
+
if let text = pasteboard.string(forType: .string) {
|
|
16
|
+
result["text"] = text
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if let data = pasteboard.data(forType: NSPasteboard.PasteboardType("org.chromium.web-custom-data")) {
|
|
20
|
+
result["pickleBase64"] = data.base64EncodedString()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if let mapData = pasteboard.data(
|
|
24
|
+
forType: NSPasteboard.PasteboardType("org.w3.web-custom-format.map")),
|
|
25
|
+
let mapString = String(data: mapData, encoding: .utf8),
|
|
26
|
+
let mapJson = try? JSONSerialization.jsonObject(with: Data(mapString.utf8)) as? [String: String],
|
|
27
|
+
let pasteboardType = mapJson["web " + GRAB_MIME] ?? mapJson[GRAB_MIME],
|
|
28
|
+
let raw = pasteboard.data(forType: NSPasteboard.PasteboardType(pasteboardType)),
|
|
29
|
+
let value = String(data: raw, encoding: .utf8)
|
|
30
|
+
{
|
|
31
|
+
result["grab"] = value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let outData = (try? JSONSerialization.data(withJSONObject: result, options: [])) ?? Data("{}".utf8)
|
|
35
|
+
FileHandle.standardOutput.write(outData)
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-grab/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"bin": {
|
|
5
5
|
"react-grab": "./bin/cli.js"
|
|
6
6
|
},
|
|
7
7
|
"files": [
|
|
8
8
|
"bin",
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"skills"
|
|
10
11
|
],
|
|
11
12
|
"type": "module",
|
|
12
13
|
"exports": {
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
21
|
+
"agent-install": "^0.0.5",
|
|
20
22
|
"commander": "^14.0.3",
|
|
21
23
|
"ignore": "^7.0.5",
|
|
22
24
|
"jsonc-parser": "^3.3.1",
|
|
@@ -29,11 +31,12 @@
|
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/prompts": "^2.4.9",
|
|
34
|
+
"cross-env": "^10.1.0",
|
|
32
35
|
"vite-plus": "^0.1.20"
|
|
33
36
|
},
|
|
34
37
|
"scripts": {
|
|
35
|
-
"dev": "
|
|
36
|
-
"build": "rm -rf dist && NODE_ENV=production vp pack",
|
|
38
|
+
"dev": "node scripts/bundle-skill.mjs --watch",
|
|
39
|
+
"build": "rm -rf dist skills && node scripts/bundle-skill.mjs && cross-env NODE_ENV=production vp pack && node scripts/copy-native-readers.mjs",
|
|
37
40
|
"test": "vp test run",
|
|
38
41
|
"test:watch": "vp test",
|
|
39
42
|
"lint": "vp lint",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-grab
|
|
3
|
+
description: >-
|
|
4
|
+
Use when the user wants a hands-free loop where grabbing UI elements in the
|
|
5
|
+
browser with React Grab feeds tasks to the agent automatically, with no
|
|
6
|
+
copy-paste or manual handoff. Triggers: "watch react grab", "monitor my
|
|
7
|
+
grabs", "auto-process react grab", "watch my clipboard for grabs". Not for a
|
|
8
|
+
one-off paste of a single grab; this is the continuous, always-on loop.
|
|
9
|
+
disable-model-invocation: true
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# React Grab
|
|
13
|
+
|
|
14
|
+
The user selects UI elements in their browser and copies them with React Grab.
|
|
15
|
+
`npx grab watch` blocks until the next grab lands on the clipboard, prints it as
|
|
16
|
+
one line of JSON, and exits. Run it, act on the grab, run it again: that is the
|
|
17
|
+
whole loop. No background process, no notifications, no polling, just a blocking
|
|
18
|
+
command you keep re-running until the user says stop.
|
|
19
|
+
|
|
20
|
+
## The loop
|
|
21
|
+
|
|
22
|
+
1. Run `npx grab watch` in the foreground. It blocks until the user grabs
|
|
23
|
+
something, then prints the grab JSON and exits 0.
|
|
24
|
+
2. Act on the grab (below).
|
|
25
|
+
3. Repeat.
|
|
26
|
+
|
|
27
|
+
Each grab is also appended to `./.react-grab/history.jsonl` as a durable record;
|
|
28
|
+
the command drops a `.gitignore` there so it never lands in git. `--dir <path>`
|
|
29
|
+
relocates it, `--text-only` skips the native clipboard reader.
|
|
30
|
+
|
|
31
|
+
## Acting on a grab
|
|
32
|
+
|
|
33
|
+
The grab JSON has `content` (the element's source references) and, in prompt
|
|
34
|
+
mode, `prompt` (the user's typed instruction):
|
|
35
|
+
|
|
36
|
+
- **`prompt` present** → that comment IS the task. Execute it against the grabbed
|
|
37
|
+
source; `content` holds the references (`// path:line`, `in Component (at …)`),
|
|
38
|
+
so jump straight to that file.
|
|
39
|
+
- **No `prompt`** → apply the standing instruction the user set when starting the
|
|
40
|
+
loop, or, if there is none, triage it (summarize component + `file:line`) and
|
|
41
|
+
wait for direction.
|
|
42
|
+
|
|
43
|
+
A standing instruction is optional; prompt mode lets the user steer each grab
|
|
44
|
+
inline.
|
|
45
|
+
|
|
46
|
+
## Stopping
|
|
47
|
+
|
|
48
|
+
When the user says stop, interrupt the command if it is still blocking and do not
|
|
49
|
+
run it again. Confirm the loop has stopped.
|