@pro-vi/designer 0.3.0
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/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/designer +35 -0
- package/dist/artifact-store.js +46 -0
- package/dist/browser.js +101 -0
- package/dist/cdp-ensure.js +44 -0
- package/dist/cli.js +618 -0
- package/dist/designer-controller.js +602 -0
- package/dist/mcp-server.js +136 -0
- package/dist/repo-root.js +15 -0
- package/dist/scripts/probe.js +62 -0
- package/dist/session-store.js +49 -0
- package/dist/setup.js +258 -0
- package/dist/tasting.js +117 -0
- package/dist/ui-anchors.js +182 -0
- package/package.json +60 -0
- package/scripts/designer-chrome.sh +38 -0
- package/selectors.json +36 -0
- package/skills/designer-loop/SKILL.md +214 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Provi Zhang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# designer
|
|
2
|
+
|
|
3
|
+
MCP + CLI for iterating on **claude.ai/design** — Claude's web-based wireframe and high-fidelity design tool.
|
|
4
|
+
|
|
5
|
+
The human describes intent. The orchestrating agent translates intent into a minimal prompt, relays it to Claude Design, hands the URL back to the human, interprets their reaction, iterates. When the human says "yes", `designer_handoff` downloads a tar.gz bundle (README + chat transcripts + all design files + source JSX) so the implementing agent (Claude Code, typically) can build the design in a real repo.
|
|
6
|
+
|
|
7
|
+
## Stance
|
|
8
|
+
|
|
9
|
+
- **Single-vendor, single-purpose.** Only `claude.ai/design`. No kitchen sink.
|
|
10
|
+
- **`agent-browser` is the substrate.** Attaches to your real Chrome via CDP — sidesteps Cloudflare + Google SSO.
|
|
11
|
+
- **Human is the designer.** See `~/.claude/skills/designer-loop`. Orchestrator is translation + plumbing; Claude Design has the taste; human arbitrates.
|
|
12
|
+
- **URL is the default taste path.** `designer_prompt` returns a live claude.ai/design URL where tweak sliders work and variants switch. Local tasting harness exists only for when IDE chrome gets in the way.
|
|
13
|
+
- **Artifacts land on disk.** Every iteration + every handoff saves under `./artifacts/{key}/`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
One-call first-run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/pro-vi/designer.git && cd designer
|
|
21
|
+
npm install
|
|
22
|
+
./bin/designer setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Optional — make `designer` available globally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm link
|
|
29
|
+
designer setup # now works from any cwd
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`designer setup` is idempotent and auto-progresses:
|
|
33
|
+
|
|
34
|
+
1. Installs deps if missing.
|
|
35
|
+
2. Checks `agent-browser` is on PATH.
|
|
36
|
+
3. Asks you to Cmd+Q Chrome if a non-debug Chrome is running (polls until quit).
|
|
37
|
+
4. Launches a dedicated debug Chrome (`--remote-debugging-port=9222`, profile at `~/.chrome-designer-profile/`).
|
|
38
|
+
5. Polls until you sign in to Claude and reach `/design`.
|
|
39
|
+
6. Installs the `designer-loop` skill at `~/.claude/skills/designer-loop/` unless one is already present (respects bootstrap/dotfiles-managed symlinks).
|
|
40
|
+
7. Registers the MCP with Claude Code at user scope.
|
|
41
|
+
|
|
42
|
+
Re-run any time — every step no-ops when already satisfied.
|
|
43
|
+
|
|
44
|
+
### Why a dedicated profile?
|
|
45
|
+
|
|
46
|
+
Since Chrome 136, `--remote-debugging-port` is blocked on the default profile for security. The dedicated `~/.chrome-designer-profile/` is a one-time login that persists across launches. Your normal Chrome is untouched.
|
|
47
|
+
|
|
48
|
+
### Auto-launch
|
|
49
|
+
|
|
50
|
+
After first setup, the MCP auto-launches debug Chrome from the saved profile on the first tool call of any session. You don't have to think about Chrome state again. If a non-debug Chrome is running, auto-launch bails with an actionable error.
|
|
51
|
+
|
|
52
|
+
### Bot detection
|
|
53
|
+
|
|
54
|
+
Login is a real human typing into a real Chrome window. `--remote-debugging-port` alone doesn't set `navigator.webdriver` (only `--enable-automation` does); user-agent is identical to normal Chrome. Cloudflare and Google OAuth see a normal session. First login on the new profile may trigger Google's "new device" verification — that's a standard one-time prompt.
|
|
55
|
+
|
|
56
|
+
### Manual setup
|
|
57
|
+
|
|
58
|
+
The MCP registration embeds `DESIGNER_CDP=9222`, so Claude sessions pick it up automatically. The shell export below only matters for direct CLI invocations (`designer session`, `designer prompt`, etc.) from an interactive terminal — add it to `~/.zshenv` or equivalent if you use the CLI directly.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
./scripts/designer-chrome.sh # launches debug Chrome
|
|
62
|
+
# sign in to Claude, navigate to /design
|
|
63
|
+
curl -s http://127.0.0.1:9222/json/version | head # verify CDP
|
|
64
|
+
export DESIGNER_CDP=9222 # only needed for direct CLI use
|
|
65
|
+
claude mcp add --transport stdio designer \
|
|
66
|
+
-- env DESIGNER_CDP=9222 "$PWD/bin/designer" mcp serve
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## CLI
|
|
70
|
+
|
|
71
|
+
Top-level help leads with the typical loop:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
$ designer --help
|
|
75
|
+
|
|
76
|
+
designer — CLI + MCP for iterating on claude.ai/design
|
|
77
|
+
|
|
78
|
+
Typical loop:
|
|
79
|
+
designer setup (once per machine)
|
|
80
|
+
designer session --action create --name "X" --key x start a project
|
|
81
|
+
designer prompt "design the …" --key x prints 'Taste here: <url>' ← open that
|
|
82
|
+
designer prompt - --key x < follow-up.txt iterate until human says yes
|
|
83
|
+
designer handoff --key x bundle for code implementation
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Verbs are grouped: session lifecycle, design operations, file introspection, exit/promotion, setup+ops, internal. Every verb supports `--help`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
designer prompt --help # expanded docs: input modes, flags, output shape, examples
|
|
90
|
+
designer help handoff # same
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
All verbs take `--key <k>` to isolate parallel sessions (e.g., working on two features at once). Local state lives at `~/.designer/sessions.json`.
|
|
94
|
+
|
|
95
|
+
Prompts accept three input modes:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
designer prompt "short text" --key feat-x # positional
|
|
99
|
+
designer prompt --prompt-file ./brief.md --key feat-x # from file
|
|
100
|
+
cat follow-up.txt | designer prompt - --key feat-x # stdin
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Output of `prompt` and `snapshot` leads with `Taste here: <url>` above the JSON — the URL is the default taste path.
|
|
104
|
+
|
|
105
|
+
## MCP
|
|
106
|
+
|
|
107
|
+
Six tools, registered at user scope by `designer setup`:
|
|
108
|
+
|
|
109
|
+
| Tool | Purpose |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `designer_session` | Enter / inspect / transition. Actions: `status` (default, read-only), `ensure_ready`, `resume`, `create`. Always returns stored state + `currentUrl` + `availableFiles`. |
|
|
112
|
+
| `designer_prompt` | Modify the design (HTML-diff wait). Auto-appends a flat-layout instruction. Returns `url` (hand to human), `newFiles`, `activeFile`, `failureMode`, `htmlPath`, `chatReply`. |
|
|
113
|
+
| `designer_ask` | Q&A with the assistant (chat-panel wait). No file changes. Returns `reply`. |
|
|
114
|
+
| `designer_list` | `scope: 'projects'` (scrapes home) or `'files'` (scrapes file panel — flat-only, see quirks). |
|
|
115
|
+
| `designer_snapshot` | Capture current state. Optional `filename` to switch first. Default: paths + hash only; `includeHtml: true` inlines. |
|
|
116
|
+
| `designer_handoff` | Export → Handoff → download + extract tar.gz. Returns README + paths. Auto-repairs Claude-side em-dash filename bugs. |
|
|
117
|
+
|
|
118
|
+
Registration:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
claude mcp add --transport stdio designer \
|
|
122
|
+
-- env DESIGNER_CDP=9222 \
|
|
123
|
+
/Users/provi/Development/_projs/designer/bin/designer mcp serve
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
(`designer setup` runs this. After `npm link` it collapses to `designer mcp serve`.)
|
|
127
|
+
|
|
128
|
+
## The loop
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
1. Intent → human describes what they want to feel / change
|
|
132
|
+
2. Read → designer_session returns currentUrl + availableFiles
|
|
133
|
+
3. Relay → designer_prompt with a terse, guide-not-constrain prompt
|
|
134
|
+
4. Taste → hand the human the url from the return; they react in their own words
|
|
135
|
+
5. Interpret → next designer_prompt (modify) or designer_ask (clarify)
|
|
136
|
+
6. Repeat 3-5 → until human says "that's it"
|
|
137
|
+
7. Promote → designer_handoff — bundle is the decision record
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Full guidance in `~/.claude/skills/designer-loop/SKILL.md` (also in-repo at `skills/designer-loop/SKILL.md`).
|
|
141
|
+
|
|
142
|
+
## Tasting harness
|
|
143
|
+
|
|
144
|
+
Fallback for when claude.ai/design's IDE chrome (chat panel + toolbar) eats too much viewport to judge at real scale. Requires a prior `designer_handoff`.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
designer tasting --key <key>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Walks the latest bundle's `project/` dir (recursively — handles nested layouts), writes `tasting.html` with variant tabs + keyboard shortcuts (1/2/3) + persistent notes (localStorage), starts a local `http.server`, opens the browser.
|
|
151
|
+
|
|
152
|
+
Default path for tasting is the live URL. Use tasting when: full-viewport comparison matters, Claude didn't build its own `index.html` gallery, or the IDE chrome is distracting.
|
|
153
|
+
|
|
154
|
+
## Operations
|
|
155
|
+
|
|
156
|
+
- `designer doctor` — diagnose first-run setup state. Checks agent-browser, CDP, a /design tab is open, selectors present, skill installed, MCP registered. Exits 2 on failure.
|
|
157
|
+
- `designer health` — probe every UI anchor this MCP depends on. 17 probes across home / session / share / pattern categories. Exits 2 on any fail. Wire into cron / CI to catch claude.ai UI regressions (it already moved Export under Share once mid-development).
|
|
158
|
+
|
|
159
|
+
## Layout
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
designer/
|
|
163
|
+
├── package.json
|
|
164
|
+
├── tsconfig.json # type-check only
|
|
165
|
+
├── tsconfig.build.json # tsc → dist/
|
|
166
|
+
├── bin/designer # bash wrapper, prefers dist/ then tsx
|
|
167
|
+
├── mcp-server.ts # MCP stdio server (exports startMcpServer)
|
|
168
|
+
├── cli.ts # same verbs, directly runnable; rich --help
|
|
169
|
+
├── designer-controller.ts # core flow: session, prompt, ask, snapshot, handoff
|
|
170
|
+
├── browser.ts # thin wrapper over agent-browser subprocess
|
|
171
|
+
├── cdp-ensure.ts # auto-launches debug Chrome on first tool call
|
|
172
|
+
├── tasting.ts # tasting.html generator + http.server
|
|
173
|
+
├── ui-anchors.ts # every DOM / URL / structural dependency, enumerated
|
|
174
|
+
├── setup.ts # designer setup verb
|
|
175
|
+
├── session-store.ts # per-session state at ~/.designer/
|
|
176
|
+
├── artifact-store.ts # writes HTML/PNG/JSON under ./artifacts/{key}/
|
|
177
|
+
├── repo-root.ts # package.json walk so source + compiled both resolve resources
|
|
178
|
+
├── selectors.json # DOM selectors for the claude.ai/design surface
|
|
179
|
+
├── scripts/
|
|
180
|
+
│ ├── designer-chrome.sh # standalone Chrome launcher
|
|
181
|
+
│ └── probe.ts # manual DOM exploration helper
|
|
182
|
+
├── skills/
|
|
183
|
+
│ └── designer-loop/SKILL.md # the skill, copied to ~/.claude/skills/ by setup
|
|
184
|
+
├── artifacts/ # generated outputs (gitignored)
|
|
185
|
+
└── dist/ # tsc build output (gitignored; published on npm)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Known quirks
|
|
189
|
+
|
|
190
|
+
- **Folder-organized variants.** Claude Design sometimes organizes multi-file variants under a subfolder (`directions/sediment.html`). The live MCP's file-list scrape (`designer_list files`, `availableFiles` in session status, `newFiles` diff from `designer_prompt`) is flat-only; nested files are invisible until `designer_handoff`. Mitigation: `designer_prompt` auto-appends *"Keep all generated files at the project root; no subfolders."* Handoff bundle is folder-aware; `designer tasting` walks recursively.
|
|
191
|
+
- **React-controlled inputs.** `agent-browser fill` doesn't fire React's synthetic input event. The controller uses the native `HTMLTextAreaElement` value-setter + `dispatchEvent(new Event('input', { bubbles: true }))`, plus JS `.click()` on Send and Create. Both are canonical React-compat patterns.
|
|
192
|
+
- **Em-dash handoff filenames.** Claude's handoff pipeline wrote em-dash (`—`) into `index.html` hrefs but saved files with regular hyphens. `designer_handoff` detects and repairs (`repaired.renamed: [...]`). No-op when hrefs already resolve.
|
|
193
|
+
- **Cross-origin iframe.** Served HTML lives on `claudeusercontent.com` with a signed `t=` token in the URL. Direct fetch from node works without cookies. The token is session-scoped, not per-iteration.
|
|
194
|
+
- **UI regressions.** Claude has moved critical buttons mid-development (Export → Share dropdown). `designer health` is the early-warning system; run it periodically.
|
|
195
|
+
|
|
196
|
+
## Publishing (future — not yet on npm)
|
|
197
|
+
|
|
198
|
+
Prepped but not shipped. Prerequisites: npm account with scope `@pro-vi` (currently 404/available), 2FA, `npm login`. Then:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm publish --access public
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`prepublishOnly` runs `tsc --noEmit` + `tsc -p tsconfig.build.json`. `files` whitelist in `package.json` keeps the tarball under 35KB. Published package will support:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npx @pro-vi/designer setup # trial
|
|
208
|
+
npm i -g @pro-vi/designer && designer setup # daily use
|
|
209
|
+
claude mcp add --transport stdio designer -- designer mcp serve # MCP
|
|
210
|
+
```
|
package/bin/designer
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper that resolves the repo's tsx + cli.ts regardless of the user's cwd.
|
|
3
|
+
# Lets users type `designer <verb>` (after `npm link` or PATH symlink)
|
|
4
|
+
# instead of the verbose `node_modules/.bin/tsx cli.ts <verb>`.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# Resolve symlinks to find the real script location, then back up to repo root.
|
|
9
|
+
SOURCE="${BASH_SOURCE[0]}"
|
|
10
|
+
while [ -h "$SOURCE" ]; do
|
|
11
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
12
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
13
|
+
[[ "$SOURCE" != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
14
|
+
done
|
|
15
|
+
BIN_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
16
|
+
REPO_ROOT="$(cd "$BIN_DIR/.." && pwd)"
|
|
17
|
+
|
|
18
|
+
DIST_CLI="$REPO_ROOT/dist/cli.js"
|
|
19
|
+
TSX="$REPO_ROOT/node_modules/.bin/tsx"
|
|
20
|
+
SRC_CLI="$REPO_ROOT/cli.ts"
|
|
21
|
+
|
|
22
|
+
# Prefer compiled output (npm-installed users + post-build dev). Fall back to
|
|
23
|
+
# tsx-on-source (clone-and-run dev mode, before tsc emits dist/).
|
|
24
|
+
if [ -f "$DIST_CLI" ]; then
|
|
25
|
+
exec node "$DIST_CLI" "$@"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -x "$TSX" ] && [ -f "$SRC_CLI" ]; then
|
|
29
|
+
exec "$TSX" "$SRC_CLI" "$@"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "[designer] No runnable found." >&2
|
|
33
|
+
echo " Expected $DIST_CLI (compiled) or $TSX + $SRC_CLI (dev)." >&2
|
|
34
|
+
echo " Run: cd $REPO_ROOT && npm install && npm run build" >&2
|
|
35
|
+
exit 1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { REPO_ROOT } from "./repo-root.js";
|
|
4
|
+
const ARTIFACTS_ROOT = process.env.DESIGNER_ARTIFACTS_DIR || path.join(REPO_ROOT, 'artifacts');
|
|
5
|
+
function slug(s) {
|
|
6
|
+
return String(s || 'session')
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
9
|
+
.replace(/^-+|-+$/g, '')
|
|
10
|
+
.slice(0, 64) || 'session';
|
|
11
|
+
}
|
|
12
|
+
function ts() {
|
|
13
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
14
|
+
}
|
|
15
|
+
export function sessionDir(key) {
|
|
16
|
+
const dir = path.join(ARTIFACTS_ROOT, slug(key));
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
export function saveIteration(key, input) {
|
|
21
|
+
const dir = sessionDir(key);
|
|
22
|
+
const stamp = ts();
|
|
23
|
+
const base = path.join(dir, stamp);
|
|
24
|
+
const record = {
|
|
25
|
+
at: new Date().toISOString(),
|
|
26
|
+
key,
|
|
27
|
+
prompt: input.prompt,
|
|
28
|
+
fidelity: input.fidelity,
|
|
29
|
+
url: input.url,
|
|
30
|
+
meta: input.meta ?? null,
|
|
31
|
+
files: {}
|
|
32
|
+
};
|
|
33
|
+
if (input.html) {
|
|
34
|
+
const p = `${base}.html`;
|
|
35
|
+
fs.writeFileSync(p, input.html);
|
|
36
|
+
record.files.html = p;
|
|
37
|
+
}
|
|
38
|
+
if (input.screenshotPath) {
|
|
39
|
+
record.files.screenshot = input.screenshotPath;
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(`${base}.json`, JSON.stringify(record, null, 2));
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
export function artifactsRoot() {
|
|
45
|
+
return ARTIFACTS_ROOT;
|
|
46
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const BIN = process.env.DESIGNER_AGENT_BROWSER_BIN || 'agent-browser';
|
|
3
|
+
const DEFAULT_SESSION = process.env.DESIGNER_SESSION_NAME || 'designer';
|
|
4
|
+
const CDP = process.env.DESIGNER_CDP || '';
|
|
5
|
+
export function createBrowser({ session = DEFAULT_SESSION, headed = true, timeoutMs = 30_000, cdp = CDP } = {}) {
|
|
6
|
+
const baseEnv = {
|
|
7
|
+
...process.env,
|
|
8
|
+
AGENT_BROWSER_DEFAULT_TIMEOUT: String(timeoutMs),
|
|
9
|
+
...(cdp ? {} : { AGENT_BROWSER_SESSION_NAME: session }),
|
|
10
|
+
...(headed && !cdp ? { AGENT_BROWSER_HEADED: '1' } : {})
|
|
11
|
+
};
|
|
12
|
+
function connectFlags() {
|
|
13
|
+
if (!cdp)
|
|
14
|
+
return [];
|
|
15
|
+
if (cdp === 'auto' || cdp === '1' || cdp === 'true')
|
|
16
|
+
return ['--auto-connect'];
|
|
17
|
+
return ['--cdp', cdp];
|
|
18
|
+
}
|
|
19
|
+
function run(args, { input, parseJson = false } = {}) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const finalArgs = [...connectFlags(), ...args];
|
|
22
|
+
const child = spawn(BIN, finalArgs, { env: baseEnv, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
23
|
+
let stdout = '';
|
|
24
|
+
let stderr = '';
|
|
25
|
+
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
26
|
+
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
27
|
+
child.on('error', (err) => reject(err));
|
|
28
|
+
child.on('close', (code) => {
|
|
29
|
+
if (code !== 0) {
|
|
30
|
+
const err = new Error(`agent-browser ${finalArgs.join(' ')} exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
31
|
+
err.code = code;
|
|
32
|
+
return reject(err);
|
|
33
|
+
}
|
|
34
|
+
if (!parseJson)
|
|
35
|
+
return resolve(stdout.trim());
|
|
36
|
+
try {
|
|
37
|
+
resolve(JSON.parse(stdout));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
reject(new Error(`Failed to parse JSON from agent-browser: ${e.message}\n--stdout--\n${stdout}`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (input != null) {
|
|
44
|
+
child.stdin.write(input);
|
|
45
|
+
child.stdin.end();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
session,
|
|
51
|
+
run,
|
|
52
|
+
open: (url) => run(['open', url]),
|
|
53
|
+
close: () => run(['close']).catch(() => null),
|
|
54
|
+
url: () => run(['get', 'url']),
|
|
55
|
+
title: () => run(['get', 'title']),
|
|
56
|
+
snapshot: ({ interactive = true, scope } = {}) => {
|
|
57
|
+
const args = ['snapshot', '--json'];
|
|
58
|
+
if (interactive)
|
|
59
|
+
args.push('-i');
|
|
60
|
+
if (scope)
|
|
61
|
+
args.push('-s', scope);
|
|
62
|
+
return run(args, { parseJson: true });
|
|
63
|
+
},
|
|
64
|
+
snapshotText: ({ interactive = true, scope } = {}) => {
|
|
65
|
+
const args = ['snapshot'];
|
|
66
|
+
if (interactive)
|
|
67
|
+
args.push('-i');
|
|
68
|
+
if (scope)
|
|
69
|
+
args.push('-s', scope);
|
|
70
|
+
return run(args);
|
|
71
|
+
},
|
|
72
|
+
click: (sel) => run(['click', sel]),
|
|
73
|
+
fill: (sel, text) => run(['fill', sel, text]),
|
|
74
|
+
type: (sel, text) => run(['type', sel, text]),
|
|
75
|
+
press: (key) => run(['press', key]),
|
|
76
|
+
getText: (sel) => run(['get', 'text', sel]),
|
|
77
|
+
getAttr: (sel, name) => run(['get', 'attr', name, sel]),
|
|
78
|
+
getHtml: (sel) => run(['get', 'html', sel]),
|
|
79
|
+
isVisible: (sel) => run(['is', 'visible', sel]).then((s) => s.trim() === 'true'),
|
|
80
|
+
waitFor: (selOrMs) => run(['wait', String(selOrMs)]),
|
|
81
|
+
waitLoad: (state = 'networkidle') => run(['wait', '--load', state]),
|
|
82
|
+
screenshot: (path, { full = false } = {}) => {
|
|
83
|
+
const args = ['screenshot'];
|
|
84
|
+
if (path)
|
|
85
|
+
args.push(path);
|
|
86
|
+
if (full)
|
|
87
|
+
args.push('--full');
|
|
88
|
+
return run(args);
|
|
89
|
+
},
|
|
90
|
+
eval: (js) => run(['eval', js]),
|
|
91
|
+
evalValue: async (js) => {
|
|
92
|
+
const out = await run(['eval', js]);
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(out);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
throw new Error(`evalValue: stdout was not JSON-parseable: ${e.message}\n--stdout--\n${out.slice(0, 500)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
const PORT = process.env.DESIGNER_CDP || '9222';
|
|
6
|
+
const PROFILE = path.join(os.homedir(), '.chrome-designer-profile');
|
|
7
|
+
const CHROME_BIN = process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
8
|
+
async function isCdpUp() {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/json/version`, { signal: AbortSignal.timeout(1500) });
|
|
11
|
+
return res.ok;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function chromeRunning() {
|
|
18
|
+
const r = spawnSync('pgrep', ['-f', 'Google Chrome.app/Contents/MacOS/Google Chrome'], { stdio: 'pipe' });
|
|
19
|
+
return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
|
|
20
|
+
}
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
}
|
|
24
|
+
export async function ensureCdpUp() {
|
|
25
|
+
if (await isCdpUp())
|
|
26
|
+
return;
|
|
27
|
+
if (!fs.existsSync(PROFILE)) {
|
|
28
|
+
throw new Error(`CDP not up on :${PORT} and no dedicated Chrome profile at ${PROFILE}. Run: designer setup`);
|
|
29
|
+
}
|
|
30
|
+
if (chromeRunning()) {
|
|
31
|
+
throw new Error(`CDP not up on :${PORT} and a non-debug Chrome is already running. Quit Chrome (Cmd+Q) and retry, or run: designer setup`);
|
|
32
|
+
}
|
|
33
|
+
if (!fs.existsSync(CHROME_BIN)) {
|
|
34
|
+
throw new Error(`CDP not up on :${PORT} and Chrome not found at ${CHROME_BIN}. Set CHROME_BIN or install Chrome.`);
|
|
35
|
+
}
|
|
36
|
+
const child = spawn(CHROME_BIN, ['--remote-debugging-port=' + PORT, '--user-data-dir=' + PROFILE, 'https://claude.ai/design'], { detached: true, stdio: 'ignore' });
|
|
37
|
+
child.unref();
|
|
38
|
+
for (let i = 0; i < 40; i++) {
|
|
39
|
+
await sleep(500);
|
|
40
|
+
if (await isCdpUp())
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Auto-launched Chrome but CDP didn't come up on :${PORT} within 20s. Check that the launched window survived, or run designer setup.`);
|
|
44
|
+
}
|