@pro-vi/designer 0.3.9 → 0.3.11
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/README.md +1 -1
- package/dist/browser.js +12 -2
- package/dist/cdp-ensure.js +3 -0
- package/dist/cdp-trace.js +509 -0
- package/dist/designer-controller.js +74 -5
- package/dist/mcp-server.js +1 -1
- package/dist/run-state.js +327 -0
- package/dist/scripts/ci-health.js +1 -0
- package/dist/scripts/trace-analyze.js +366 -0
- package/dist/scripts/trace-spike.js +274 -0
- package/dist/setup.js +33 -2
- package/dist/ui-anchors.js +140 -2
- package/package.json +6 -4
package/dist/setup.js
CHANGED
|
@@ -162,7 +162,14 @@ async function step3Chrome(port) {
|
|
|
162
162
|
log('chrome', 'fail', `Chrome not found at ${CHROME_BIN}. Set CHROME_BIN to override.`);
|
|
163
163
|
return false;
|
|
164
164
|
}
|
|
165
|
-
const child = spawn(CHROME_BIN, [
|
|
165
|
+
const child = spawn(CHROME_BIN, [
|
|
166
|
+
'--remote-debugging-port=' + port,
|
|
167
|
+
'--user-data-dir=' + PROFILE,
|
|
168
|
+
'--no-first-run',
|
|
169
|
+
'--no-default-browser-check',
|
|
170
|
+
'--disable-search-engine-choice-screen',
|
|
171
|
+
'https://claude.ai/design'
|
|
172
|
+
], {
|
|
166
173
|
detached: true,
|
|
167
174
|
stdio: 'ignore'
|
|
168
175
|
});
|
|
@@ -184,7 +191,28 @@ async function step4SignIn(port) {
|
|
|
184
191
|
const browser = createBrowser({ session: 'designer-setup', cdp: port });
|
|
185
192
|
await browser.open('https://claude.ai/design').catch(() => undefined);
|
|
186
193
|
await sleep(2500);
|
|
187
|
-
const
|
|
194
|
+
const MAX_RECOVERY_NAVS = 4;
|
|
195
|
+
const hasAuthCookie = async () => {
|
|
196
|
+
try {
|
|
197
|
+
return (await browser.cookies()).some((c) => /^sessionKey/.test(c.name) && /claude\.ai$/.test(c.domain) && c.value.length > 20);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
let recoveryNavs = 0;
|
|
204
|
+
const checkSignedIn = async () => {
|
|
205
|
+
if (await verifySignedIn(browser))
|
|
206
|
+
return true;
|
|
207
|
+
if (recoveryNavs < MAX_RECOVERY_NAVS && (await hasAuthCookie())) {
|
|
208
|
+
recoveryNavs++;
|
|
209
|
+
await browser.open('https://claude.ai/design').catch(() => undefined);
|
|
210
|
+
await sleep(3000);
|
|
211
|
+
return verifySignedIn(browser);
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
215
|
+
const ok = await pollUntil('login', checkSignedIn, {
|
|
188
216
|
intervalMs: 2000,
|
|
189
217
|
timeoutMs: 10 * 60_000,
|
|
190
218
|
reminder: 'Sign in to Claude in the DEBUG Chrome window I just opened (a separate window with no extensions/bookmarks — NOT your normal Chrome; the two have separate cookie jars). Then return to claude.ai/design. I am polling.',
|
|
@@ -192,6 +220,9 @@ async function step4SignIn(port) {
|
|
|
192
220
|
});
|
|
193
221
|
if (!ok) {
|
|
194
222
|
log('login', 'fail', 'Timed out waiting for a signed-in claude.ai/design session. Re-run setup when ready.');
|
|
223
|
+
const watched = await browser.url().catch(() => '(unreachable)');
|
|
224
|
+
const authCookie = await hasAuthCookie();
|
|
225
|
+
log('login', 'fail', `Watched tab: ${watched} | Claude auth cookie: ${authCookie ? 'present — login succeeded but the tab stayed stale; re-run: designer setup' : 'absent (or watched tab is off claude.ai origin) — see watched URL above'}`);
|
|
195
226
|
return false;
|
|
196
227
|
}
|
|
197
228
|
const url = (await browser.url().catch(() => '')) || 'claude.ai/design';
|
package/dist/ui-anchors.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RunStateObserver } from "./run-state.js";
|
|
1
2
|
async function hasSelector(browser, sel) {
|
|
2
3
|
return !!(await browser
|
|
3
4
|
.evalValue(`!!document.querySelector(${JSON.stringify(sel)})`)
|
|
@@ -8,7 +9,112 @@ async function hasButtonMatching(browser, pattern) {
|
|
|
8
9
|
.evalValue(`(() => { const re = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(pattern.flags)}); return Array.from(document.querySelectorAll('button')).some(b => re.test((b.textContent || '').trim())); })()`)
|
|
9
10
|
.catch(() => false));
|
|
10
11
|
}
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
async function submitTurnRpcCanary(browser) {
|
|
16
|
+
const prompt = 'Health check: answer in chat only with the single word ok. Do not create, modify, or delete files.';
|
|
17
|
+
const filled = await browser
|
|
18
|
+
.evalValue(`(() => {
|
|
19
|
+
const el = document.querySelector('[data-testid="chat-composer-input"]');
|
|
20
|
+
if (!el) return false;
|
|
21
|
+
const text = ${JSON.stringify(prompt)};
|
|
22
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
23
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
|
|
24
|
+
setter.call(el, text);
|
|
25
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
26
|
+
el.focus();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (el.isContentEditable) {
|
|
30
|
+
el.focus();
|
|
31
|
+
const sel = window.getSelection();
|
|
32
|
+
const range = document.createRange();
|
|
33
|
+
range.selectNodeContents(el);
|
|
34
|
+
sel.removeAllRanges();
|
|
35
|
+
sel.addRange(range);
|
|
36
|
+
const dt = new DataTransfer();
|
|
37
|
+
dt.setData('text/plain', text);
|
|
38
|
+
const unhandled = el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true }));
|
|
39
|
+
if (unhandled) document.execCommand('insertText', false, text);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
})()`)
|
|
44
|
+
.catch(() => false);
|
|
45
|
+
if (!filled)
|
|
46
|
+
return { ok: false, detail: 'composer not fillable for canary prompt' };
|
|
47
|
+
for (let i = 0; i < 30; i++) {
|
|
48
|
+
const disabled = await browser
|
|
49
|
+
.evalValue(`(() => {
|
|
50
|
+
const b = document.querySelector('[data-testid="chat-send-button"], button[title^="Send ("]');
|
|
51
|
+
return !b || b.disabled || b.getAttribute('aria-disabled') === 'true';
|
|
52
|
+
})()`)
|
|
53
|
+
.catch(() => true);
|
|
54
|
+
if (!disabled)
|
|
55
|
+
break;
|
|
56
|
+
await sleep(150);
|
|
57
|
+
}
|
|
58
|
+
const clicked = await browser
|
|
59
|
+
.evalValue(`(() => {
|
|
60
|
+
const b = document.querySelector('[data-testid="chat-send-button"], button[title^="Send ("]');
|
|
61
|
+
if (!b || b.disabled || b.getAttribute('aria-disabled') === 'true') return false;
|
|
62
|
+
b.click();
|
|
63
|
+
return true;
|
|
64
|
+
})()`)
|
|
65
|
+
.catch(() => false);
|
|
66
|
+
return clicked ? { ok: true } : { ok: false, detail: 'send button unavailable for canary prompt' };
|
|
67
|
+
}
|
|
68
|
+
async function checkTurnRpcContract(_browser, currentUrl) {
|
|
69
|
+
if (process.env.DESIGNER_TURN_RPC_CANARY !== '1') {
|
|
70
|
+
return { ok: true, status: 'skip', detail: 'turn-RPC canary disabled (DESIGNER_TURN_RPC_CANARY!=1)' };
|
|
71
|
+
}
|
|
72
|
+
if ((process.env.DESIGNER_CDP ?? '9222') === '') {
|
|
73
|
+
return { ok: true, status: 'skip', detail: "CDP disabled (DESIGNER_CDP=''); turn-RPC canary not probed" };
|
|
74
|
+
}
|
|
75
|
+
const observer = await RunStateObserver.attach({ preferUrlPrefix: currentUrl.split('?')[0] || null });
|
|
76
|
+
if (!observer) {
|
|
77
|
+
return { ok: true, status: 'skip', detail: 'CDP observer unavailable; turn-RPC canary not probed' };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
observer.beginRun();
|
|
81
|
+
const submitted = await submitTurnRpcCanary(_browser);
|
|
82
|
+
if (!submitted.ok)
|
|
83
|
+
return { ok: true, status: 'skip', detail: submitted.detail };
|
|
84
|
+
const terminal = await observer.awaitTerminal({ stallMs: 25_000, hardTimeoutMs: 75_000 });
|
|
85
|
+
const summary = observer.signalSummary();
|
|
86
|
+
const detail = `heartbeat x${summary.heartbeat}, release ${summary.release > 0 ? 'seen' : 'missing'}, ` +
|
|
87
|
+
`chat x${summary.chatOpen}, chunks x${summary.chatChunk}, terminal=${terminal.terminal}` +
|
|
88
|
+
(summary.observedRpcPaths.length ? `, observed=[${summary.observedRpcPaths.join(', ')}]` : ', observed=[]');
|
|
89
|
+
return {
|
|
90
|
+
ok: terminal.terminal === 'finished' && summary.release > 0 && summary.chatOpen > 0,
|
|
91
|
+
detail
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
observer.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
11
98
|
export const UI_ANCHORS = [
|
|
99
|
+
{
|
|
100
|
+
id: 'login.signedIn',
|
|
101
|
+
category: 'pattern',
|
|
102
|
+
description: 'signed in (claude.ai is rendering the app shell, not the login wall)',
|
|
103
|
+
requires: 'any',
|
|
104
|
+
check: async (b, url) => {
|
|
105
|
+
if (/claude\.ai\/login/.test(url)) {
|
|
106
|
+
return { ok: false, detail: `signed out — Chrome is on the login wall (${url.slice(0, 80)}). Run: designer setup` };
|
|
107
|
+
}
|
|
108
|
+
if (/claude\.ai\/design/.test(url)) {
|
|
109
|
+
const signedIn = (await hasSelector(b, '[data-testid="project-creator"]')) ||
|
|
110
|
+
(await hasSelector(b, '[data-testid="chat-composer-input"]'));
|
|
111
|
+
return signedIn
|
|
112
|
+
? { ok: true }
|
|
113
|
+
: { ok: false, detail: `login wall rendered at ${url.slice(0, 80)} (no app shell) — signed out. Run: designer setup` };
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, detail: `not on a claude.ai/design surface (url=${url.slice(0, 60)}) — sign-in not checked here` };
|
|
116
|
+
}
|
|
117
|
+
},
|
|
12
118
|
{
|
|
13
119
|
id: 'home.creator',
|
|
14
120
|
category: 'home',
|
|
@@ -65,6 +171,31 @@ export const UI_ANCHORS = [
|
|
|
65
171
|
requires: 'session',
|
|
66
172
|
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-composer-input"]') })
|
|
67
173
|
},
|
|
174
|
+
{
|
|
175
|
+
id: 'session.composerFillable',
|
|
176
|
+
category: 'session',
|
|
177
|
+
description: 'composer is fillable (textarea or contenteditable, per _submitPrompt)',
|
|
178
|
+
requires: 'session',
|
|
179
|
+
check: async (b) => {
|
|
180
|
+
const shape = await b
|
|
181
|
+
.evalValue(`(() => {
|
|
182
|
+
const el = document.querySelector('[data-testid="chat-composer-input"]');
|
|
183
|
+
if (!el) return { found: false };
|
|
184
|
+
const fillable = el instanceof HTMLTextAreaElement || el.isContentEditable;
|
|
185
|
+
return { found: true, tag: el.tagName, contentEditable: el.isContentEditable, fillable };
|
|
186
|
+
})()`)
|
|
187
|
+
.catch(() => ({ found: false }));
|
|
188
|
+
if (!shape.found)
|
|
189
|
+
return { ok: false, detail: 'composer not found' };
|
|
190
|
+
if (shape.fillable) {
|
|
191
|
+
return { ok: true, detail: shape.contentEditable ? 'contenteditable' : `<${(shape.tag || '').toLowerCase()}>` };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
detail: `composer is <${(shape.tag || '?').toLowerCase()}> — neither textarea nor contenteditable; _submitPrompt cannot fill it (composer shape drifted)`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
},
|
|
68
199
|
{
|
|
69
200
|
id: 'session.sendButton',
|
|
70
201
|
category: 'session',
|
|
@@ -90,6 +221,13 @@ export const UI_ANCHORS = [
|
|
|
90
221
|
requires: 'session',
|
|
91
222
|
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-messages"]') })
|
|
92
223
|
},
|
|
224
|
+
{
|
|
225
|
+
id: 'network.turnRpcContract',
|
|
226
|
+
category: 'pattern',
|
|
227
|
+
description: 'OmeletteService Chat/RenewTurn/ReleaseTurn network contract',
|
|
228
|
+
requires: 'session',
|
|
229
|
+
check: checkTurnRpcContract
|
|
230
|
+
},
|
|
93
231
|
{
|
|
94
232
|
id: 'session.iframeSrcPattern',
|
|
95
233
|
category: 'pattern',
|
|
@@ -228,7 +366,7 @@ export async function runHealth(browser, opts = {}) {
|
|
|
228
366
|
};
|
|
229
367
|
try {
|
|
230
368
|
const r = await a.check(browser, currentUrl);
|
|
231
|
-
results.push({ ...base, status: r.ok ? 'ok' : 'fail', detail: r.detail });
|
|
369
|
+
results.push({ ...base, status: r.status ?? (r.ok ? 'ok' : 'fail'), detail: r.detail });
|
|
232
370
|
}
|
|
233
371
|
catch (e) {
|
|
234
372
|
results.push({ ...base, status: 'fail', detail: `threw: ${e.message}` });
|
|
@@ -251,7 +389,7 @@ export async function runHealth(browser, opts = {}) {
|
|
|
251
389
|
}
|
|
252
390
|
try {
|
|
253
391
|
const r = await a.check(browser, currentUrl);
|
|
254
|
-
results.push({ ...base, status: r.ok ? 'ok' : 'fail', detail: r.detail });
|
|
392
|
+
results.push({ ...base, status: r.status ?? (r.ok ? 'ok' : 'fail'), detail: r.detail });
|
|
255
393
|
}
|
|
256
394
|
catch (e) {
|
|
257
395
|
results.push({ ...base, status: 'fail', detail: `threw: ${e.message}` });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pro-vi/designer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP + CLI for autonomous iteration of claude.ai/design — drives the design surface via agent-browser, downloads handoff bundles, and exposes a tasting harness for full-viewport variant comparison.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"cli": "tsx cli.ts",
|
|
31
31
|
"setup": "tsx cli.ts setup",
|
|
32
32
|
"doctor": "tsx cli.ts doctor",
|
|
33
|
-
"test": "npm run build && node --test tests/cli-metadata.test.mjs",
|
|
33
|
+
"test": "npm run build && node --test tests/cli-metadata.test.mjs && node --import tsx --test tests/run-state.*.test.mjs tests/cdp-trace.*.test.mjs",
|
|
34
34
|
"check": "tsc --noEmit",
|
|
35
35
|
"build": "tsc -p tsconfig.build.json",
|
|
36
36
|
"prepack": "npm run check && npm run build",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"probe": "tsx scripts/probe.ts",
|
|
39
39
|
"probe:health": "tsx scripts/ci-health.ts",
|
|
40
40
|
"auto-heal": "tsx scripts/auto-heal.ts",
|
|
41
|
-
"smoke": "bash scripts/install-smoke.sh"
|
|
41
|
+
"smoke": "bash scripts/install-smoke.sh",
|
|
42
|
+
"trace": "tsx scripts/trace-spike.ts",
|
|
43
|
+
"trace:analyze": "tsx scripts/trace-analyze.ts"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
44
46
|
"@anthropic-ai/sdk": "^0.102.0",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
"typescript": "^6.0.3"
|
|
54
56
|
},
|
|
55
57
|
"engines": {
|
|
56
|
-
"node": ">=
|
|
58
|
+
"node": ">=22"
|
|
57
59
|
},
|
|
58
60
|
"os": [
|
|
59
61
|
"darwin",
|