@pro-vi/designer 0.3.10 → 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/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/ui-anchors.js +96 -2
- package/package.json +6 -4
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --import tsx
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DesignerController } from "../designer-controller.js";
|
|
5
|
+
import { CdpTraceRecorder } from "../cdp-trace.js";
|
|
6
|
+
import { artifactsRoot } from "../artifact-store.js";
|
|
7
|
+
import { getSession } from "../session-store.js";
|
|
8
|
+
const USAGE = `Usage:
|
|
9
|
+
trace-spike.ts quota [--seconds 60] capture quota banner + short idle trace
|
|
10
|
+
trace-spike.ts idle [--minutes 3] baseline noise trace
|
|
11
|
+
trace-spike.ts success "<prompt>" [--key K] [--name N] [--fidelity highfi|wireframe] [--decisive] [--sample-ms 1500]
|
|
12
|
+
trace-spike.ts noop ["<prompt>"] [--key K] chat-only prompt (expects no file change)
|
|
13
|
+
trace-spike.ts watch [--key K] record until Ctrl-C (opportunistic capture)
|
|
14
|
+
|
|
15
|
+
Traces land in artifacts/trace/<scenario>-<ts>/{trace.jsonl,manifest.json}.`;
|
|
16
|
+
function parseArgv(argv) {
|
|
17
|
+
const positional = [];
|
|
18
|
+
const flags = {};
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (!a)
|
|
22
|
+
continue;
|
|
23
|
+
if (a.startsWith('--')) {
|
|
24
|
+
const name = a.slice(2);
|
|
25
|
+
const next = argv[i + 1];
|
|
26
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
27
|
+
flags[name] = next;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
flags[name] = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
positional.push(a);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { positional, flags };
|
|
39
|
+
}
|
|
40
|
+
function escapeRegExp(s) {
|
|
41
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
}
|
|
43
|
+
const NOOP_PROMPT = 'Answer in chat only — do not create, modify, or delete any files: briefly describe what the current design does.';
|
|
44
|
+
function buildSampleJs(c) {
|
|
45
|
+
const sel = c.selectors;
|
|
46
|
+
return `(() => {
|
|
47
|
+
const q = (s) => { try { return document.querySelector(s); } catch { return null; } };
|
|
48
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
49
|
+
const composer = q(${JSON.stringify(sel.composer.promptTextarea)});
|
|
50
|
+
const send = q(${JSON.stringify(sel.composer.sendButton)});
|
|
51
|
+
const iframe = q(${JSON.stringify(sel.preview.iframeOrContainer)});
|
|
52
|
+
const msgs = q(${JSON.stringify(sel.messages.chatMessagesContainer)});
|
|
53
|
+
let chatTurnCount = 0; let lastTurnRole = null;
|
|
54
|
+
if (msgs) {
|
|
55
|
+
const turns = msgs.querySelectorAll('[data-index]');
|
|
56
|
+
chatTurnCount = turns.length;
|
|
57
|
+
const last = turns[turns.length - 1];
|
|
58
|
+
if (last) {
|
|
59
|
+
const t = (last.innerText || '').trim();
|
|
60
|
+
lastTurnRole = t.startsWith('Claude') ? 'assistant' : t.startsWith('You') ? 'user' : 'unknown';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// selectors.json has composer.stopButton: null — probe generically so the
|
|
64
|
+
// trace doubles as selector discovery for the real stop button.
|
|
65
|
+
let stopProbe = null;
|
|
66
|
+
for (const b of Array.from(document.querySelectorAll('button'))) {
|
|
67
|
+
const label = ((b.getAttribute('aria-label') || '') + ' ' + (b.textContent || '')).trim();
|
|
68
|
+
if (/\\b(stop|cancel)\\b/i.test(label) && vis(b)) {
|
|
69
|
+
stopProbe = {
|
|
70
|
+
label: label.slice(0, 80),
|
|
71
|
+
testid: b.getAttribute('data-testid'),
|
|
72
|
+
outerHTML: b.outerHTML.slice(0, 400)
|
|
73
|
+
};
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
url: location.href,
|
|
79
|
+
iframeSrc: iframe && iframe.src ? iframe.src : null,
|
|
80
|
+
composerVisible: vis(composer),
|
|
81
|
+
sendVisible: vis(send),
|
|
82
|
+
sendDisabled: send ? (send.disabled === true || send.getAttribute('aria-disabled') === 'true') : null,
|
|
83
|
+
chatTurnCount,
|
|
84
|
+
lastTurnRole,
|
|
85
|
+
stopProbe
|
|
86
|
+
};
|
|
87
|
+
})()`;
|
|
88
|
+
}
|
|
89
|
+
const QUOTA_BANNER_JS = `(() => {
|
|
90
|
+
// Find the smallest element whose text mentions a percentage AND
|
|
91
|
+
// weekly-limit language — that's the usage banner.
|
|
92
|
+
const all = Array.from(document.querySelectorAll('div, section, aside'));
|
|
93
|
+
let best = null;
|
|
94
|
+
for (const el of all) {
|
|
95
|
+
const t = (el.innerText || '').trim();
|
|
96
|
+
if (!t || t.length > 600) continue;
|
|
97
|
+
if (!/\\d+\\s*%/.test(t)) continue;
|
|
98
|
+
if (!/week|usage|limit|resets/i.test(t)) continue;
|
|
99
|
+
if (!best || t.length < (best.innerText || '').trim().length) best = el;
|
|
100
|
+
}
|
|
101
|
+
if (!best) return { found: false, text: null, outerHTML: null };
|
|
102
|
+
return { found: true, text: (best.innerText || '').trim(), outerHTML: best.outerHTML.slice(0, 8000) };
|
|
103
|
+
})()`;
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
106
|
+
}
|
|
107
|
+
async function main() {
|
|
108
|
+
const { positional, flags } = parseArgv(process.argv.slice(2));
|
|
109
|
+
const scenario = positional[0] || '';
|
|
110
|
+
if (!['quota', 'idle', 'success', 'noop', 'watch'].includes(scenario)) {
|
|
111
|
+
console.log(USAGE);
|
|
112
|
+
process.exit(scenario ? 1 : 0);
|
|
113
|
+
}
|
|
114
|
+
const key = String(flags.key || 'trace-spike');
|
|
115
|
+
const sampleMs = Number(flags['sample-ms'] || (scenario === 'idle' || scenario === 'watch' ? 5000 : 1500));
|
|
116
|
+
const controller = new DesignerController({ key });
|
|
117
|
+
const ready = await controller.ensureReady();
|
|
118
|
+
console.log(`ready: ${ready.url}`);
|
|
119
|
+
let prompt = null;
|
|
120
|
+
if (scenario === 'success' || scenario === 'noop') {
|
|
121
|
+
prompt = positional[1] || (scenario === 'noop' ? NOOP_PROMPT : null);
|
|
122
|
+
if (!prompt) {
|
|
123
|
+
console.error('success requires a prompt argument');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const stored = getSession(key);
|
|
127
|
+
if (stored?.designUrl) {
|
|
128
|
+
await controller.resumeSession();
|
|
129
|
+
}
|
|
130
|
+
else if (scenario === 'noop') {
|
|
131
|
+
console.error(`No stored session for key=${key} — run a success scenario first so noop has a design to ask about.`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const name = String(flags.name || 'aurora-trace');
|
|
136
|
+
const fidelity = flags.fidelity === 'wireframe' ? 'wireframe' : 'highfi';
|
|
137
|
+
const created = await controller.createSession(name, fidelity);
|
|
138
|
+
console.log(`created session: ${created.url}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
142
|
+
const outDir = path.join(artifactsRoot(), 'trace', `${scenario}-${stamp}`);
|
|
143
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
144
|
+
const targetUrlFlag = typeof flags['target-url'] === 'string' ? String(flags['target-url']) : null;
|
|
145
|
+
const recorder = await CdpTraceRecorder.attach({
|
|
146
|
+
outFile: path.join(outDir, 'trace.jsonl'),
|
|
147
|
+
preferUrlPrefix: getSession(key)?.designUrl?.split('?')[0] || null,
|
|
148
|
+
...(targetUrlFlag ? { urlPattern: new RegExp('^' + escapeRegExp(targetUrlFlag)) } : {})
|
|
149
|
+
});
|
|
150
|
+
await recorder.start();
|
|
151
|
+
console.log(`recording → ${outDir}`);
|
|
152
|
+
const manifest = {
|
|
153
|
+
scenario,
|
|
154
|
+
key,
|
|
155
|
+
prompt,
|
|
156
|
+
startedAt: new Date().toISOString(),
|
|
157
|
+
endedAt: null,
|
|
158
|
+
aborted: false,
|
|
159
|
+
node: process.version,
|
|
160
|
+
cdp: recorder.targetInfo(),
|
|
161
|
+
iterate: null,
|
|
162
|
+
quota: null,
|
|
163
|
+
summary: null
|
|
164
|
+
};
|
|
165
|
+
const sampleJs = buildSampleJs(controller);
|
|
166
|
+
let samplerInFlight = false;
|
|
167
|
+
const sampler = setInterval(() => {
|
|
168
|
+
if (samplerInFlight)
|
|
169
|
+
return;
|
|
170
|
+
samplerInFlight = true;
|
|
171
|
+
controller.browser
|
|
172
|
+
.evalValue(sampleJs)
|
|
173
|
+
.then((sample) => recorder.record({ ts: Date.now(), kind: 'dom-sample', sample }))
|
|
174
|
+
.catch(() => null)
|
|
175
|
+
.finally(() => {
|
|
176
|
+
samplerInFlight = false;
|
|
177
|
+
});
|
|
178
|
+
}, sampleMs);
|
|
179
|
+
let finished = false;
|
|
180
|
+
const finalize = async (aborted) => {
|
|
181
|
+
if (finished)
|
|
182
|
+
return;
|
|
183
|
+
finished = true;
|
|
184
|
+
clearInterval(sampler);
|
|
185
|
+
manifest.aborted = aborted;
|
|
186
|
+
manifest.endedAt = new Date().toISOString();
|
|
187
|
+
manifest.summary = await recorder.stop();
|
|
188
|
+
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
189
|
+
console.log(`\ntrace: ${path.join(outDir, 'trace.jsonl')}`);
|
|
190
|
+
console.log(`manifest: ${path.join(outDir, 'manifest.json')}`);
|
|
191
|
+
console.log(`events: ${manifest.summary.total} | bodies: ${manifest.summary.bodyCaptures} | reconnects: ${manifest.summary.reconnects}`);
|
|
192
|
+
};
|
|
193
|
+
process.on('SIGINT', () => {
|
|
194
|
+
void finalize(true).then(() => process.exit(130));
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
if (scenario === 'quota') {
|
|
198
|
+
const screenshotPath = path.join(outDir, 'quota-banner.png');
|
|
199
|
+
await controller.browser.screenshot(screenshotPath, { full: true }).catch((e) => {
|
|
200
|
+
console.warn(`screenshot failed: ${e.message}`);
|
|
201
|
+
return '';
|
|
202
|
+
});
|
|
203
|
+
const banner = await controller.browser
|
|
204
|
+
.evalValue(QUOTA_BANNER_JS)
|
|
205
|
+
.catch(() => ({ found: false, text: null, outerHTML: null }));
|
|
206
|
+
let bannerHtmlPath = null;
|
|
207
|
+
if (banner.found && banner.outerHTML) {
|
|
208
|
+
bannerHtmlPath = path.join(outDir, 'quota-banner.html');
|
|
209
|
+
fs.writeFileSync(bannerHtmlPath, banner.outerHTML);
|
|
210
|
+
}
|
|
211
|
+
manifest.quota = {
|
|
212
|
+
bannerText: banner.text,
|
|
213
|
+
bannerHtmlPath,
|
|
214
|
+
screenshotPath: fs.existsSync(screenshotPath) ? screenshotPath : null
|
|
215
|
+
};
|
|
216
|
+
console.log(banner.found ? `banner: ${banner.text}` : 'banner: NOT FOUND (see screenshot)');
|
|
217
|
+
const seconds = Number(flags.seconds || 60);
|
|
218
|
+
recorder.marker('quota-idle-start', { seconds });
|
|
219
|
+
await sleep(seconds * 1000);
|
|
220
|
+
recorder.marker('quota-idle-end');
|
|
221
|
+
}
|
|
222
|
+
else if (scenario === 'idle') {
|
|
223
|
+
const minutes = Number(flags.minutes || 3);
|
|
224
|
+
recorder.marker('idle-start', { minutes });
|
|
225
|
+
await sleep(minutes * 60_000);
|
|
226
|
+
recorder.marker('idle-end');
|
|
227
|
+
}
|
|
228
|
+
else if (scenario === 'success' || scenario === 'noop') {
|
|
229
|
+
recorder.marker('iterate-start', { prompt, decisive: flags.decisive === true });
|
|
230
|
+
const result = await controller.iterate(prompt, { decisive: flags.decisive === true });
|
|
231
|
+
recorder.marker('iterate-done', {
|
|
232
|
+
failureMode: result.done.failureMode,
|
|
233
|
+
elapsedMs: result.done.elapsedMs,
|
|
234
|
+
changed: result.changed,
|
|
235
|
+
newFiles: result.newFiles
|
|
236
|
+
});
|
|
237
|
+
manifest.iterate = {
|
|
238
|
+
failureMode: result.done.failureMode,
|
|
239
|
+
ok: result.done.ok,
|
|
240
|
+
elapsedMs: result.done.elapsedMs,
|
|
241
|
+
changed: result.changed,
|
|
242
|
+
newFiles: result.newFiles,
|
|
243
|
+
removedFiles: result.removedFiles,
|
|
244
|
+
activeFile: result.activeFile,
|
|
245
|
+
htmlBytes: result.htmlBytes,
|
|
246
|
+
chatReplyBytes: result.chatReply ? result.chatReply.length : 0
|
|
247
|
+
};
|
|
248
|
+
console.log(`iterate: ok=${result.done.ok} failureMode=${result.done.failureMode} elapsed=${Math.round(result.done.elapsedMs / 1000)}s newFiles=[${result.newFiles.join(', ')}]`);
|
|
249
|
+
const banner = await controller.browser
|
|
250
|
+
.evalValue(QUOTA_BANNER_JS)
|
|
251
|
+
.catch(() => ({ found: false, text: null, outerHTML: null }));
|
|
252
|
+
if (banner.found && banner.outerHTML) {
|
|
253
|
+
const bannerHtmlPath = path.join(outDir, 'quota-banner.html');
|
|
254
|
+
fs.writeFileSync(bannerHtmlPath, banner.outerHTML);
|
|
255
|
+
manifest.quota = { bannerText: banner.text, bannerHtmlPath, screenshotPath: null };
|
|
256
|
+
console.log(`quota banner captured: ${banner.text}`);
|
|
257
|
+
}
|
|
258
|
+
await sleep(5000);
|
|
259
|
+
}
|
|
260
|
+
else if (scenario === 'watch') {
|
|
261
|
+
recorder.marker('watch-start');
|
|
262
|
+
console.log('watching — Ctrl-C to stop');
|
|
263
|
+
await new Promise(() => {
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await finalize(false);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
main().catch(async (e) => {
|
|
272
|
+
console.error(e.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
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,6 +9,92 @@ 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 = [
|
|
12
99
|
{
|
|
13
100
|
id: 'login.signedIn',
|
|
@@ -134,6 +221,13 @@ export const UI_ANCHORS = [
|
|
|
134
221
|
requires: 'session',
|
|
135
222
|
check: async (b) => ({ ok: await hasSelector(b, '[data-testid="chat-messages"]') })
|
|
136
223
|
},
|
|
224
|
+
{
|
|
225
|
+
id: 'network.turnRpcContract',
|
|
226
|
+
category: 'pattern',
|
|
227
|
+
description: 'OmeletteService Chat/RenewTurn/ReleaseTurn network contract',
|
|
228
|
+
requires: 'session',
|
|
229
|
+
check: checkTurnRpcContract
|
|
230
|
+
},
|
|
137
231
|
{
|
|
138
232
|
id: 'session.iframeSrcPattern',
|
|
139
233
|
category: 'pattern',
|
|
@@ -272,7 +366,7 @@ export async function runHealth(browser, opts = {}) {
|
|
|
272
366
|
};
|
|
273
367
|
try {
|
|
274
368
|
const r = await a.check(browser, currentUrl);
|
|
275
|
-
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 });
|
|
276
370
|
}
|
|
277
371
|
catch (e) {
|
|
278
372
|
results.push({ ...base, status: 'fail', detail: `threw: ${e.message}` });
|
|
@@ -295,7 +389,7 @@ export async function runHealth(browser, opts = {}) {
|
|
|
295
389
|
}
|
|
296
390
|
try {
|
|
297
391
|
const r = await a.check(browser, currentUrl);
|
|
298
|
-
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 });
|
|
299
393
|
}
|
|
300
394
|
catch (e) {
|
|
301
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",
|