cdp-skill 1.0.7 → 1.0.14
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 +80 -35
- package/SKILL.md +198 -1344
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +268 -68
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +34 -143
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +256 -95
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -740
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +34 -736
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- package/src/utils.js +14 -1142
package/src/cdp-skill.js
CHANGED
|
@@ -60,9 +60,9 @@ function generateDebugFilename(steps, status, tabId) {
|
|
|
60
60
|
const actions = steps.slice(0, 3).map(step => {
|
|
61
61
|
// Find the action key in the step
|
|
62
62
|
const actionKeys = ['goto', 'click', 'fill', 'type', 'press', 'scroll', 'snapshot',
|
|
63
|
-
'query', 'hover', 'wait', '
|
|
63
|
+
'query', 'hover', 'wait', 'sleep', 'pageFunction', 'newTab', 'closeTab',
|
|
64
64
|
'selectOption', 'select', 'viewport', 'cookies', 'back', 'forward', 'drag',
|
|
65
|
-
'
|
|
65
|
+
'frame', 'elementsAt', 'extract', 'formState', 'assert', 'validate', 'submit'];
|
|
66
66
|
for (const key of actionKeys) {
|
|
67
67
|
if (step[key] !== undefined) return key;
|
|
68
68
|
}
|
|
@@ -95,7 +95,7 @@ function writeDebugLog(request, response) {
|
|
|
95
95
|
const status = response.status || 'unknown';
|
|
96
96
|
|
|
97
97
|
// Extract tab ID from request or response
|
|
98
|
-
const tabId = request.tab ||
|
|
98
|
+
const tabId = request.tab || response.tab || response.closed || null;
|
|
99
99
|
|
|
100
100
|
const filename = generateDebugFilename(steps, status, tabId);
|
|
101
101
|
const filepath = path.join(debugLogDir, filename);
|
|
@@ -114,9 +114,56 @@ function writeDebugLog(request, response) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Tab registry - maps short aliases (t1, t2, ...) to
|
|
117
|
+
// Tab registry - maps short aliases (t1, t2, ...) to {targetId, host, port} entries
|
|
118
118
|
const TAB_REGISTRY_PATH = path.join(os.tmpdir(), 'cdp-skill-tabs.json');
|
|
119
119
|
|
|
120
|
+
// Frame state registry - persists frame context across CLI invocations, keyed by targetId
|
|
121
|
+
const FRAME_STATE_PATH = path.join(os.tmpdir(), 'cdp-skill-frames.json');
|
|
122
|
+
|
|
123
|
+
function loadFrameStates() {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(FRAME_STATE_PATH)) {
|
|
126
|
+
return JSON.parse(fs.readFileSync(FRAME_STATE_PATH, 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Ignore errors, start fresh
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function saveFrameStates(states) {
|
|
135
|
+
try {
|
|
136
|
+
fs.writeFileSync(FRAME_STATE_PATH, JSON.stringify(states));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// Ignore errors
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function saveFrameState(targetId, frameState) {
|
|
143
|
+
const states = loadFrameStates();
|
|
144
|
+
states[targetId] = { ...frameState, timestamp: Date.now() };
|
|
145
|
+
saveFrameStates(states);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadFrameState(targetId) {
|
|
149
|
+
const states = loadFrameStates();
|
|
150
|
+
const state = states[targetId];
|
|
151
|
+
if (!state) return null;
|
|
152
|
+
// Expire after 1 hour (frames may have reloaded)
|
|
153
|
+
if (Date.now() - state.timestamp > 3600000) {
|
|
154
|
+
delete states[targetId];
|
|
155
|
+
saveFrameStates(states);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return state;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function clearFrameState(targetId) {
|
|
162
|
+
const states = loadFrameStates();
|
|
163
|
+
delete states[targetId];
|
|
164
|
+
saveFrameStates(states);
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
function loadTabRegistry() {
|
|
121
168
|
try {
|
|
122
169
|
if (fs.existsSync(TAB_REGISTRY_PATH)) {
|
|
@@ -136,22 +183,43 @@ function saveTabRegistry(registry) {
|
|
|
136
183
|
}
|
|
137
184
|
}
|
|
138
185
|
|
|
139
|
-
function registerTab(targetId) {
|
|
186
|
+
function registerTab(targetId, host = 'localhost', port = 9222) {
|
|
140
187
|
const registry = loadTabRegistry();
|
|
141
188
|
|
|
142
189
|
// Check if already registered
|
|
143
|
-
for (const [alias,
|
|
144
|
-
|
|
190
|
+
for (const [alias, entry] of Object.entries(registry.tabs)) {
|
|
191
|
+
const existingTargetId = typeof entry === 'string' ? entry : entry.targetId;
|
|
192
|
+
if (existingTargetId === targetId) return alias;
|
|
145
193
|
}
|
|
146
194
|
|
|
147
195
|
// Assign new alias
|
|
148
196
|
const alias = `t${registry.nextId}`;
|
|
149
|
-
registry.tabs[alias] = targetId;
|
|
197
|
+
registry.tabs[alias] = { targetId, host, port };
|
|
150
198
|
registry.nextId++;
|
|
151
199
|
saveTabRegistry(registry);
|
|
152
200
|
return alias;
|
|
153
201
|
}
|
|
154
202
|
|
|
203
|
+
function resolveTabEntry(aliasOrTargetId) {
|
|
204
|
+
if (!aliasOrTargetId) return null;
|
|
205
|
+
|
|
206
|
+
// If it looks like a full targetId (32 hex chars), return with defaults
|
|
207
|
+
if (/^[A-F0-9]{32}$/i.test(aliasOrTargetId)) {
|
|
208
|
+
return { targetId: aliasOrTargetId, host: 'localhost', port: 9222 };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const registry = loadTabRegistry();
|
|
212
|
+
const entry = registry.tabs[aliasOrTargetId];
|
|
213
|
+
if (!entry) return null;
|
|
214
|
+
|
|
215
|
+
// Defensive: handle stale registry files with string entries
|
|
216
|
+
if (typeof entry === 'string') {
|
|
217
|
+
return { targetId: entry, host: 'localhost', port: 9222 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { targetId: entry.targetId, host: entry.host || 'localhost', port: entry.port || 9222 };
|
|
221
|
+
}
|
|
222
|
+
|
|
155
223
|
function resolveTabAlias(aliasOrTargetId) {
|
|
156
224
|
if (!aliasOrTargetId) return null;
|
|
157
225
|
|
|
@@ -162,15 +230,21 @@ function resolveTabAlias(aliasOrTargetId) {
|
|
|
162
230
|
|
|
163
231
|
// Try to resolve alias
|
|
164
232
|
const registry = loadTabRegistry();
|
|
165
|
-
|
|
233
|
+
const entry = registry.tabs[aliasOrTargetId];
|
|
234
|
+
if (!entry) return aliasOrTargetId;
|
|
235
|
+
|
|
236
|
+
// Defensive: handle stale registry files with string entries
|
|
237
|
+
return typeof entry === 'string' ? entry : entry.targetId;
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
function unregisterTab(targetId) {
|
|
169
241
|
const registry = loadTabRegistry();
|
|
170
|
-
for (const [alias,
|
|
171
|
-
|
|
242
|
+
for (const [alias, entry] of Object.entries(registry.tabs)) {
|
|
243
|
+
const existingTargetId = typeof entry === 'string' ? entry : entry.targetId;
|
|
244
|
+
if (existingTargetId === targetId) {
|
|
172
245
|
delete registry.tabs[alias];
|
|
173
246
|
saveTabRegistry(registry);
|
|
247
|
+
clearFrameState(targetId);
|
|
174
248
|
return alias;
|
|
175
249
|
}
|
|
176
250
|
}
|
|
@@ -179,8 +253,9 @@ function unregisterTab(targetId) {
|
|
|
179
253
|
|
|
180
254
|
function getTabAlias(targetId) {
|
|
181
255
|
const registry = loadTabRegistry();
|
|
182
|
-
for (const [alias,
|
|
183
|
-
|
|
256
|
+
for (const [alias, entry] of Object.entries(registry.tabs)) {
|
|
257
|
+
const existingTargetId = typeof entry === 'string' ? entry : entry.targetId;
|
|
258
|
+
if (existingTargetId === targetId) return alias;
|
|
184
259
|
}
|
|
185
260
|
return null;
|
|
186
261
|
}
|
|
@@ -283,6 +358,13 @@ function parseInput(input) {
|
|
|
283
358
|
throw { type: ErrorType.VALIDATION, message: 'Input must be a JSON object' };
|
|
284
359
|
}
|
|
285
360
|
|
|
361
|
+
if (json.config) {
|
|
362
|
+
throw {
|
|
363
|
+
type: ErrorType.VALIDATION,
|
|
364
|
+
message: '"config" is no longer supported. Use top-level "tab"/"timeout". Connection params go in newTab: {"steps":[{"newTab":{"url":"...","port":9333}}]}'
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
286
368
|
if (!json.steps) {
|
|
287
369
|
throw { type: ErrorType.VALIDATION, message: 'Missing required "steps" array' };
|
|
288
370
|
}
|
|
@@ -325,11 +407,12 @@ function isCloseTabOnly(steps) {
|
|
|
325
407
|
/**
|
|
326
408
|
* Handle chromeStatus step - lightweight, no session needed
|
|
327
409
|
*/
|
|
328
|
-
async function handleChromeStatus(
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
const
|
|
410
|
+
async function handleChromeStatus(step) {
|
|
411
|
+
const params = typeof step.chromeStatus === 'object' ? step.chromeStatus : {};
|
|
412
|
+
const host = params.host || 'localhost';
|
|
413
|
+
const port = params.port || 9222;
|
|
414
|
+
const autoLaunch = step.chromeStatus === true || params.autoLaunch !== false;
|
|
415
|
+
const headless = params.headless || false;
|
|
333
416
|
|
|
334
417
|
const status = await getChromeStatus({ host, port, autoLaunch, headless });
|
|
335
418
|
|
|
@@ -351,9 +434,7 @@ async function handleChromeStatus(config, step) {
|
|
|
351
434
|
/**
|
|
352
435
|
* Handle closeTab step - no session needed, just close the target via CDP
|
|
353
436
|
*/
|
|
354
|
-
async function handleCloseTab(
|
|
355
|
-
const host = config.host || 'localhost';
|
|
356
|
-
const port = config.port || 9222;
|
|
437
|
+
async function handleCloseTab(step) {
|
|
357
438
|
const tabRef = step.closeTab;
|
|
358
439
|
|
|
359
440
|
if (!tabRef || typeof tabRef !== 'string') {
|
|
@@ -363,8 +444,11 @@ async function handleCloseTab(config, step) {
|
|
|
363
444
|
};
|
|
364
445
|
}
|
|
365
446
|
|
|
366
|
-
// Resolve alias to targetId
|
|
367
|
-
const
|
|
447
|
+
// Resolve alias to full entry (targetId + host + port)
|
|
448
|
+
const entry = resolveTabEntry(tabRef);
|
|
449
|
+
const targetId = entry ? entry.targetId : tabRef;
|
|
450
|
+
const host = entry ? entry.host : 'localhost';
|
|
451
|
+
const port = entry ? entry.port : 9222;
|
|
368
452
|
const alias = getTabAlias(targetId);
|
|
369
453
|
|
|
370
454
|
try {
|
|
@@ -374,6 +458,7 @@ async function handleCloseTab(config, step) {
|
|
|
374
458
|
|
|
375
459
|
await new Promise((resolve, reject) => {
|
|
376
460
|
const req = http.get(closeUrl, (res) => {
|
|
461
|
+
res.resume(); // Drain response body to prevent memory leak
|
|
377
462
|
if (res.statusCode === 200) {
|
|
378
463
|
resolve();
|
|
379
464
|
} else {
|
|
@@ -409,6 +494,7 @@ async function handleCloseTab(config, step) {
|
|
|
409
494
|
* Main CLI execution
|
|
410
495
|
*/
|
|
411
496
|
async function main() {
|
|
497
|
+
const startTime = Date.now();
|
|
412
498
|
let browser = null;
|
|
413
499
|
let pageController = null;
|
|
414
500
|
let parsedRequest = null; // Track for debug logging in error handler
|
|
@@ -419,17 +505,16 @@ async function main() {
|
|
|
419
505
|
const json = parseInput(input);
|
|
420
506
|
parsedRequest = json; // Store for error handler
|
|
421
507
|
|
|
422
|
-
// Extract
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const tab = json.tab || config.tab; // Top-level tab takes precedence
|
|
508
|
+
// Extract top-level fields
|
|
509
|
+
const tab = json.tab || null;
|
|
510
|
+
const timeout = json.timeout || 30000;
|
|
511
|
+
let host = 'localhost';
|
|
512
|
+
let port = 9222;
|
|
513
|
+
let headless = false;
|
|
429
514
|
|
|
430
515
|
// Handle chromeStatus specially - no session needed
|
|
431
516
|
if (isChromeStatusOnly(json.steps)) {
|
|
432
|
-
const result = await handleChromeStatus(
|
|
517
|
+
const result = await handleChromeStatus(json.steps[0]);
|
|
433
518
|
writeDebugLog(json, result);
|
|
434
519
|
console.log(JSON.stringify(result));
|
|
435
520
|
process.exit(result.status === 'ok' ? 0 : 1);
|
|
@@ -437,12 +522,55 @@ async function main() {
|
|
|
437
522
|
|
|
438
523
|
// Handle closeTab specially - no session needed, just close the target
|
|
439
524
|
if (isCloseTabOnly(json.steps)) {
|
|
440
|
-
const result = await handleCloseTab(
|
|
525
|
+
const result = await handleCloseTab(json.steps[0]);
|
|
441
526
|
writeDebugLog(json, result);
|
|
442
527
|
console.log(JSON.stringify(result));
|
|
443
528
|
process.exit(result.status === 'ok' ? 0 : 1);
|
|
444
529
|
}
|
|
445
530
|
|
|
531
|
+
// Check if first step is newTab or switchTab
|
|
532
|
+
const firstStep = json.steps[0];
|
|
533
|
+
const hasNewTab = firstStep && firstStep.newTab !== undefined;
|
|
534
|
+
const hasSwitchTab = firstStep && firstStep.switchTab !== undefined;
|
|
535
|
+
|
|
536
|
+
// Extract URL and options from newTab if provided
|
|
537
|
+
let newTabUrl = null;
|
|
538
|
+
let newTabTimeout = null;
|
|
539
|
+
if (hasNewTab) {
|
|
540
|
+
const newTabParam = firstStep.newTab;
|
|
541
|
+
if (typeof newTabParam === 'string') {
|
|
542
|
+
newTabUrl = newTabParam;
|
|
543
|
+
} else if (typeof newTabParam === 'object' && newTabParam !== null) {
|
|
544
|
+
newTabUrl = newTabParam.url || null;
|
|
545
|
+
newTabTimeout = newTabParam.timeout || null;
|
|
546
|
+
// Extract connection overrides from newTab object form
|
|
547
|
+
if (newTabParam.host) host = newTabParam.host;
|
|
548
|
+
if (newTabParam.port) port = newTabParam.port;
|
|
549
|
+
if (newTabParam.headless) headless = newTabParam.headless;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Extract connection overrides from switchTab object form
|
|
554
|
+
if (hasSwitchTab) {
|
|
555
|
+
const switchParam = firstStep.switchTab;
|
|
556
|
+
if (typeof switchParam === 'object' && switchParam !== null) {
|
|
557
|
+
if (switchParam.host) host = switchParam.host;
|
|
558
|
+
if (switchParam.port) port = switchParam.port;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If tab specified, resolve host/port from registry
|
|
563
|
+
if (tab) {
|
|
564
|
+
const tabEntry = resolveTabEntry(tab);
|
|
565
|
+
if (tabEntry) {
|
|
566
|
+
host = tabEntry.host;
|
|
567
|
+
port = tabEntry.port;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Resolve tab alias to targetId
|
|
572
|
+
const resolvedTargetId = tab ? resolveTabAlias(tab) : null;
|
|
573
|
+
|
|
446
574
|
// Connect to browser, auto-launch if needed
|
|
447
575
|
browser = createBrowser({ host, port, connectTimeout: timeout });
|
|
448
576
|
|
|
@@ -468,27 +596,9 @@ async function main() {
|
|
|
468
596
|
}
|
|
469
597
|
}
|
|
470
598
|
|
|
471
|
-
// Get page session - requires explicit targetId or
|
|
599
|
+
// Get page session - requires explicit targetId or newTab step
|
|
472
600
|
let session;
|
|
473
601
|
|
|
474
|
-
// Check if first step is openTab
|
|
475
|
-
const firstStep = json.steps[0];
|
|
476
|
-
const hasOpenTab = firstStep && firstStep.openTab !== undefined;
|
|
477
|
-
|
|
478
|
-
// Extract URL from openTab if provided
|
|
479
|
-
let openTabUrl = null;
|
|
480
|
-
if (hasOpenTab) {
|
|
481
|
-
const openTabParam = firstStep.openTab;
|
|
482
|
-
if (typeof openTabParam === 'string') {
|
|
483
|
-
openTabUrl = openTabParam;
|
|
484
|
-
} else if (typeof openTabParam === 'object' && openTabParam !== null && openTabParam.url) {
|
|
485
|
-
openTabUrl = openTabParam.url;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Resolve tab alias to targetId
|
|
490
|
-
const resolvedTargetId = tab ? resolveTabAlias(tab) : null;
|
|
491
|
-
|
|
492
602
|
if (resolvedTargetId) {
|
|
493
603
|
try {
|
|
494
604
|
session = await browser.attachToPage(resolvedTargetId);
|
|
@@ -498,16 +608,56 @@ async function main() {
|
|
|
498
608
|
message: `Could not attach to tab ${tab}${tab !== resolvedTargetId ? ` (${resolvedTargetId})` : ''}: ${err.message}`
|
|
499
609
|
};
|
|
500
610
|
}
|
|
501
|
-
} else if (
|
|
502
|
-
//
|
|
611
|
+
} else if (hasSwitchTab) {
|
|
612
|
+
// Connect to an existing tab by alias, targetId, or URL regex
|
|
503
613
|
try {
|
|
504
|
-
|
|
614
|
+
const switchParam = firstStep.switchTab;
|
|
615
|
+
let switchTargetId = null;
|
|
616
|
+
|
|
617
|
+
if (typeof switchParam === 'string') {
|
|
618
|
+
// Try alias first, then targetId
|
|
619
|
+
switchTargetId = resolveTabAlias(switchParam);
|
|
620
|
+
} else if (switchParam && typeof switchParam === 'object') {
|
|
621
|
+
if (switchParam.targetId) {
|
|
622
|
+
switchTargetId = switchParam.targetId;
|
|
623
|
+
} else if (switchParam.url) {
|
|
624
|
+
// Find tab by URL regex
|
|
625
|
+
const pages = await browser.getPages();
|
|
626
|
+
const urlRegex = new RegExp(switchParam.url);
|
|
627
|
+
const match = pages.find(p => urlRegex.test(p.url));
|
|
628
|
+
if (!match) {
|
|
629
|
+
throw new Error(`No tab matches URL pattern: ${switchParam.url}`);
|
|
630
|
+
}
|
|
631
|
+
switchTargetId = match.targetId;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!switchTargetId) {
|
|
636
|
+
throw new Error('Could not resolve switchTab target');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
session = await browser.attachToPage(switchTargetId);
|
|
640
|
+
const tabAlias = getTabAlias(switchTargetId) || registerTab(switchTargetId, host, port);
|
|
641
|
+
json.steps[0]._switchTabHandled = true;
|
|
642
|
+
json.steps[0]._switchTabAlias = tabAlias;
|
|
643
|
+
} catch (err) {
|
|
644
|
+
throw {
|
|
645
|
+
type: ErrorType.CONNECTION,
|
|
646
|
+
message: `switchTab failed: ${err.message}`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
} else if (hasNewTab) {
|
|
650
|
+
// Create new tab via newTab step
|
|
651
|
+
try {
|
|
652
|
+
// Create blank tab - URL navigation happens in step executor
|
|
653
|
+
session = await browser.newPage('about:blank');
|
|
505
654
|
// Register the new tab and get its alias
|
|
506
|
-
const tabAlias = registerTab(session.targetId);
|
|
507
|
-
// Mark
|
|
508
|
-
json.steps[0].
|
|
509
|
-
json.steps[0].
|
|
510
|
-
json.steps[0].
|
|
655
|
+
const tabAlias = registerTab(session.targetId, host, port);
|
|
656
|
+
// Mark newTab as handled and store URL/alias/timeout if provided
|
|
657
|
+
json.steps[0]._newTabHandled = true;
|
|
658
|
+
json.steps[0]._newTabUrl = newTabUrl;
|
|
659
|
+
json.steps[0]._newTabTimeout = newTabTimeout;
|
|
660
|
+
json.steps[0]._newTabAlias = tabAlias;
|
|
511
661
|
} catch (err) {
|
|
512
662
|
if (err.message.includes('no browser is open')) {
|
|
513
663
|
throw {
|
|
@@ -521,23 +671,28 @@ async function main() {
|
|
|
521
671
|
};
|
|
522
672
|
}
|
|
523
673
|
} else {
|
|
524
|
-
// No targetId and no
|
|
674
|
+
// No targetId and no newTab/switchTab step - fail with helpful message
|
|
525
675
|
throw {
|
|
526
676
|
type: ErrorType.VALIDATION,
|
|
527
677
|
message: `No tab specified. Either:\n` +
|
|
528
|
-
` 1. Use {"steps":[{"
|
|
529
|
-
` 2.
|
|
678
|
+
` 1. Use {"steps":[{"newTab":"url"},...]} to create a new tab\n` +
|
|
679
|
+
` 2. Use {"steps":[{"switchTab":"t1"},...]} to connect to an existing tab\n` +
|
|
680
|
+
` 3. Pass tab id: {"tab":"t1", "steps":[...]}`
|
|
530
681
|
};
|
|
531
682
|
}
|
|
532
683
|
|
|
533
684
|
// Create dependencies
|
|
534
|
-
pageController = createPageController(session
|
|
535
|
-
|
|
685
|
+
pageController = createPageController(session, {
|
|
686
|
+
onFrameChanged: (frameState) => saveFrameState(session.targetId, frameState),
|
|
687
|
+
getSavedFrameState: () => loadFrameState(session.targetId)
|
|
688
|
+
});
|
|
689
|
+
const frameContextProvider = () => pageController.getFrameContext();
|
|
690
|
+
const elementLocator = createElementLocator(session, { getFrameContext: frameContextProvider });
|
|
536
691
|
const inputEmulator = createInputEmulator(session);
|
|
537
692
|
const screenshotCapture = createScreenshotCapture(session);
|
|
538
693
|
const consoleCapture = createConsoleCapture(session);
|
|
539
694
|
const pdfCapture = createPdfCapture(session);
|
|
540
|
-
const ariaSnapshot = createAriaSnapshot(session);
|
|
695
|
+
const ariaSnapshot = createAriaSnapshot(session, { getFrameContext: frameContextProvider });
|
|
541
696
|
const cookieManager = createCookieManager(session);
|
|
542
697
|
|
|
543
698
|
// Initialize page controller (enables required CDP domains)
|
|
@@ -558,11 +713,12 @@ async function main() {
|
|
|
558
713
|
consoleCapture,
|
|
559
714
|
pdfCapture,
|
|
560
715
|
ariaSnapshot,
|
|
561
|
-
cookieManager
|
|
716
|
+
cookieManager,
|
|
717
|
+
registerNewTab: (targetId) => registerTab(targetId, host, port)
|
|
562
718
|
};
|
|
563
719
|
|
|
564
720
|
// Run steps (pass tab alias for auto-screenshots)
|
|
565
|
-
const tabAlias = getTabAlias(session.targetId) || registerTab(session.targetId);
|
|
721
|
+
const tabAlias = getTabAlias(session.targetId) || registerTab(session.targetId, host, port);
|
|
566
722
|
const result = await runSteps(deps, json.steps, {
|
|
567
723
|
stopOnError: true,
|
|
568
724
|
stepTimeout: timeout,
|
|
@@ -586,10 +742,32 @@ async function main() {
|
|
|
586
742
|
screenshotPath = null;
|
|
587
743
|
}
|
|
588
744
|
|
|
745
|
+
// Bubble up site profile info from goto/newTab steps to top level
|
|
746
|
+
let siteProfile = undefined;
|
|
747
|
+
let actionRequired = undefined;
|
|
748
|
+
for (const step of result.steps) {
|
|
749
|
+
if (step.siteProfile) {
|
|
750
|
+
siteProfile = step.siteProfile;
|
|
751
|
+
delete step.siteProfile;
|
|
752
|
+
} else if (step.profileAvailable === false) {
|
|
753
|
+
actionRequired = {
|
|
754
|
+
action: 'createSiteProfile',
|
|
755
|
+
domain: step.profileDomain,
|
|
756
|
+
message: `STOP: No site profile for "${step.profileDomain}". You MUST create one before doing anything else. Run these steps now: (1) snapshot — see page structure, (2) pageFunction — detect framework/routing, (3) writeSiteProfile — save what you learned. Then continue your task. Skipping this causes unreliable automation.`
|
|
757
|
+
};
|
|
758
|
+
delete step.profileAvailable;
|
|
759
|
+
delete step.profileDomain;
|
|
760
|
+
delete step.hint;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
589
764
|
// Build streamlined output
|
|
590
765
|
const output = {
|
|
591
766
|
status: result.status,
|
|
592
|
-
tab: getTabAlias(session.targetId) || registerTab(session.targetId),
|
|
767
|
+
tab: getTabAlias(session.targetId) || registerTab(session.targetId, host, port),
|
|
768
|
+
// Site profile — prominent, right after status/tab
|
|
769
|
+
siteProfile,
|
|
770
|
+
actionRequired,
|
|
593
771
|
// Command-level auto-snapshot results
|
|
594
772
|
navigated: result.navigated,
|
|
595
773
|
fullSnapshot: result.fullSnapshot,
|
|
@@ -606,6 +784,8 @@ async function main() {
|
|
|
606
784
|
};
|
|
607
785
|
|
|
608
786
|
// Remove null/undefined fields for compactness
|
|
787
|
+
if (!output.siteProfile) delete output.siteProfile;
|
|
788
|
+
if (!output.actionRequired) delete output.actionRequired;
|
|
609
789
|
if (output.navigated === undefined) delete output.navigated;
|
|
610
790
|
if (!output.fullSnapshot) delete output.fullSnapshot;
|
|
611
791
|
if (!output.context) delete output.context;
|
|
@@ -631,6 +811,26 @@ async function main() {
|
|
|
631
811
|
// Debug logging
|
|
632
812
|
writeDebugLog(json, finalOutput);
|
|
633
813
|
|
|
814
|
+
// Write metrics if CDP_METRICS_FILE is set
|
|
815
|
+
const metricsFile = process.env.CDP_METRICS_FILE;
|
|
816
|
+
if (metricsFile) {
|
|
817
|
+
const inputBytes = Buffer.byteLength(input, 'utf8');
|
|
818
|
+
const outputJson = JSON.stringify(finalOutput);
|
|
819
|
+
const outputBytes = Buffer.byteLength(outputJson, 'utf8');
|
|
820
|
+
const metricsLine = JSON.stringify({
|
|
821
|
+
ts: new Date().toISOString(),
|
|
822
|
+
input_bytes: inputBytes,
|
|
823
|
+
output_bytes: outputBytes,
|
|
824
|
+
steps: json.steps.length,
|
|
825
|
+
time_ms: Date.now() - startTime
|
|
826
|
+
}) + '\n';
|
|
827
|
+
try {
|
|
828
|
+
const metricsDir = path.dirname(metricsFile);
|
|
829
|
+
if (!fs.existsSync(metricsDir)) fs.mkdirSync(metricsDir, { recursive: true });
|
|
830
|
+
fs.appendFileSync(metricsFile, metricsLine);
|
|
831
|
+
} catch (e) { /* metrics write failure is non-fatal */ }
|
|
832
|
+
}
|
|
833
|
+
|
|
634
834
|
// Output result
|
|
635
835
|
console.log(JSON.stringify(finalOutput));
|
|
636
836
|
|