cdp-skill 1.0.18 → 1.0.20
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/EXAMPLES.md +2 -2
- package/SKILL.md +2 -2
- package/package.json +1 -1
- package/scripts/cdp-skill.js +3 -3
- package/scripts/runner/execute-browser.js +8 -2
- package/scripts/runner/execute-dynamic.js +6 -5
- package/scripts/runner/execute-query.js +23 -14
- package/scripts/runner/step-executors.js +12 -5
- package/scripts/tests/ExecuteDynamic.test.js +1 -1
- package/scripts/tests/ExecuteQuery.test.js +20 -20
package/EXAMPLES.md
CHANGED
|
@@ -68,7 +68,7 @@ Close when done:
|
|
|
68
68
|
},
|
|
69
69
|
"screenshot": "/tmp/cdp-skill/t1.after.png",
|
|
70
70
|
"fullSnapshot": "/tmp/cdp-skill/t1.after.yaml",
|
|
71
|
-
"viewportSnapshot": "-
|
|
71
|
+
"viewportSnapshot": "/tmp/cdp-skill/t1.viewport.yaml",
|
|
72
72
|
"steps": [{"action": "goto", "status": "ok"}]
|
|
73
73
|
}
|
|
74
74
|
```
|
|
@@ -130,7 +130,7 @@ Close when done:
|
|
|
130
130
|
},
|
|
131
131
|
"screenshot": "/tmp/cdp-skill/t1.after.png",
|
|
132
132
|
"fullSnapshot": "/tmp/cdp-skill/t1.after.yaml",
|
|
133
|
-
"viewportSnapshot": "-
|
|
133
|
+
"viewportSnapshot": "/tmp/cdp-skill/t1.viewport.yaml",
|
|
134
134
|
"steps": [{"action": "click", "status": "ok"}]
|
|
135
135
|
}
|
|
136
136
|
```
|
package/SKILL.md
CHANGED
|
@@ -33,12 +33,12 @@ Tab IDs (t1, t2, ...) persist across CLI invocations. Chrome auto-launches if no
|
|
|
33
33
|
**Output fields:**
|
|
34
34
|
- `status`: "ok" or "error"
|
|
35
35
|
- `tab`: short tab ID (e.g. "t1")
|
|
36
|
-
- `siteProfile`:
|
|
36
|
+
- `siteProfile`: path to site profile file (after goto/newTab to known site)
|
|
37
37
|
- `actionRequired`: `{action, domain, message}` — **MUST be handled immediately** before continuing (see Site Profiles)
|
|
38
38
|
- `context`: `{url, title, scroll: {y, percent}, viewport: {width, height}, activeElement?, modal?}`
|
|
39
39
|
- `screenshot`: path to after-screenshot (auto-captured on every visual action)
|
|
40
40
|
- `fullSnapshot`: path to full-page accessibility snapshot file
|
|
41
|
-
- `viewportSnapshot`:
|
|
41
|
+
- `viewportSnapshot`: path to viewport-only accessibility snapshot file
|
|
42
42
|
- `changes`: `{summary, added[], removed[], changed[]}` — viewport diff on same-page interactions
|
|
43
43
|
- `navigated`: true when URL pathname changed
|
|
44
44
|
- `console`: `{errors, warnings, messages[]}` — captured errors/warnings
|
package/package.json
CHANGED
package/scripts/cdp-skill.js
CHANGED
|
@@ -785,15 +785,15 @@ async function main() {
|
|
|
785
785
|
truncated: result.truncated
|
|
786
786
|
};
|
|
787
787
|
|
|
788
|
-
// Remove null/undefined fields for compactness
|
|
788
|
+
// Remove null/undefined/false-y fields for compactness
|
|
789
789
|
if (!output.siteProfile) delete output.siteProfile;
|
|
790
790
|
if (!output.actionRequired) delete output.actionRequired;
|
|
791
|
-
if (output.navigated
|
|
791
|
+
if (!output.navigated) delete output.navigated;
|
|
792
792
|
if (!output.fullSnapshot) delete output.fullSnapshot;
|
|
793
793
|
if (!output.context) delete output.context;
|
|
794
794
|
if (!output.changes) delete output.changes;
|
|
795
795
|
if (!output.viewportSnapshot) delete output.viewportSnapshot;
|
|
796
|
-
if (output.truncated
|
|
796
|
+
if (!output.truncated) delete output.truncated;
|
|
797
797
|
if (!output.screenshot) delete output.screenshot;
|
|
798
798
|
if (!output.console) delete output.console;
|
|
799
799
|
if (output.errors.length === 0) delete output.errors;
|
|
@@ -96,8 +96,14 @@ export async function executeEval(pageController, params) {
|
|
|
96
96
|
reject(new Error(`Eval timed out after ${evalTimeout}ms`));
|
|
97
97
|
}, evalTimeout);
|
|
98
98
|
});
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
try {
|
|
100
|
+
result = await Promise.race([evalPromise, timeoutPromise]);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
evalPromise.catch(() => {}); // suppress dangling rejection if timeout won
|
|
103
|
+
throw err;
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(evalTimeoutId);
|
|
106
|
+
}
|
|
101
107
|
} else {
|
|
102
108
|
result = await evalPromise;
|
|
103
109
|
}
|
|
@@ -408,13 +408,14 @@ function sanitizeDomain(domain) {
|
|
|
408
408
|
* Load a site profile for the given domain.
|
|
409
409
|
*
|
|
410
410
|
* @param {string} domain - hostname (e.g. "github.com")
|
|
411
|
-
* @returns {Promise<string|null>} profile
|
|
411
|
+
* @returns {Promise<string|null>} profile file path or null
|
|
412
412
|
*/
|
|
413
413
|
export async function loadSiteProfile(domain) {
|
|
414
414
|
const clean = sanitizeDomain(domain);
|
|
415
415
|
const profilePath = path.join(SITES_DIR, `${clean}.md`);
|
|
416
416
|
try {
|
|
417
|
-
|
|
417
|
+
await fs.access(profilePath);
|
|
418
|
+
return profilePath;
|
|
418
419
|
} catch {
|
|
419
420
|
return null;
|
|
420
421
|
}
|
|
@@ -452,9 +453,9 @@ export async function executeReadSiteProfile(params) {
|
|
|
452
453
|
throw new Error('readSiteProfile requires a domain string');
|
|
453
454
|
}
|
|
454
455
|
|
|
455
|
-
const
|
|
456
|
-
if (
|
|
457
|
-
return { found: true, domain,
|
|
456
|
+
const profilePath = await loadSiteProfile(domain);
|
|
457
|
+
if (profilePath) {
|
|
458
|
+
return { found: true, domain, path: profilePath };
|
|
458
459
|
}
|
|
459
460
|
return { found: false, domain };
|
|
460
461
|
}
|
|
@@ -119,7 +119,7 @@ export async function executeGetDom(pageController, params) {
|
|
|
119
119
|
const expression = selector
|
|
120
120
|
? `(function() {
|
|
121
121
|
const el = document.querySelector(${JSON.stringify(selector)});
|
|
122
|
-
if (!el) return { error: 'Element not found: ${selector}
|
|
122
|
+
if (!el) return { error: 'Element not found: ' + ${JSON.stringify(selector)} };
|
|
123
123
|
return {
|
|
124
124
|
html: ${outer} ? el.outerHTML : el.innerHTML,
|
|
125
125
|
tagName: el.tagName.toLowerCase(),
|
|
@@ -228,10 +228,11 @@ export async function executeGetBox(ariaSnapshot, params) {
|
|
|
228
228
|
* @returns {Promise<Object>} Result with filled element info
|
|
229
229
|
*/
|
|
230
230
|
|
|
231
|
-
export async function executeRefAt(
|
|
231
|
+
export async function executeRefAt(pageController, params) {
|
|
232
|
+
const session = pageController.session;
|
|
232
233
|
const { x, y } = params;
|
|
233
234
|
|
|
234
|
-
const
|
|
235
|
+
const evalArgs = {
|
|
235
236
|
expression: `(function() {
|
|
236
237
|
const x = ${x};
|
|
237
238
|
const y = ${y};
|
|
@@ -352,7 +353,10 @@ export async function executeRefAt(session, params) {
|
|
|
352
353
|
};
|
|
353
354
|
})()`,
|
|
354
355
|
returnByValue: true
|
|
355
|
-
}
|
|
356
|
+
};
|
|
357
|
+
const contextId = pageController.getFrameContext();
|
|
358
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
359
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
356
360
|
|
|
357
361
|
if (result.exceptionDetails) {
|
|
358
362
|
throw new Error(`refAt error: ${result.exceptionDetails.text}`);
|
|
@@ -370,8 +374,9 @@ export async function executeRefAt(session, params) {
|
|
|
370
374
|
* Execute an elementsAt step - get refs for elements at multiple coordinates
|
|
371
375
|
*/
|
|
372
376
|
|
|
373
|
-
export async function executeElementsAt(
|
|
374
|
-
const
|
|
377
|
+
export async function executeElementsAt(pageController, coords) {
|
|
378
|
+
const session = pageController.session;
|
|
379
|
+
const evalArgs = {
|
|
375
380
|
expression: `(function() {
|
|
376
381
|
const coords = ${JSON.stringify(coords)};
|
|
377
382
|
|
|
@@ -499,7 +504,10 @@ export async function executeElementsAt(session, coords) {
|
|
|
499
504
|
return { elements: results, count: results.filter(r => !r.error).length };
|
|
500
505
|
})()`,
|
|
501
506
|
returnByValue: true
|
|
502
|
-
}
|
|
507
|
+
};
|
|
508
|
+
const contextId = pageController.getFrameContext();
|
|
509
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
510
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
503
511
|
|
|
504
512
|
if (result.exceptionDetails) {
|
|
505
513
|
throw new Error(`elementsAt error: ${result.exceptionDetails.text}`);
|
|
@@ -512,10 +520,11 @@ export async function executeElementsAt(session, coords) {
|
|
|
512
520
|
* Execute an elementsNear step - get refs for all elements near a coordinate
|
|
513
521
|
*/
|
|
514
522
|
|
|
515
|
-
export async function executeElementsNear(
|
|
523
|
+
export async function executeElementsNear(pageController, params) {
|
|
524
|
+
const session = pageController.session;
|
|
516
525
|
const { x, y, radius = 50, limit = 20 } = params;
|
|
517
526
|
|
|
518
|
-
const
|
|
527
|
+
const evalArgs = {
|
|
519
528
|
expression: `(function() {
|
|
520
529
|
const centerX = ${x};
|
|
521
530
|
const centerY = ${y};
|
|
@@ -665,7 +674,10 @@ export async function executeElementsNear(session, params) {
|
|
|
665
674
|
};
|
|
666
675
|
})()`,
|
|
667
676
|
returnByValue: true
|
|
668
|
-
}
|
|
677
|
+
};
|
|
678
|
+
const contextId = pageController.getFrameContext();
|
|
679
|
+
if (contextId) evalArgs.contextId = contextId;
|
|
680
|
+
const result = await session.send('Runtime.evaluate', evalArgs);
|
|
669
681
|
|
|
670
682
|
if (result.exceptionDetails) {
|
|
671
683
|
throw new Error(`elementsNear error: ${result.exceptionDetails.text}`);
|
|
@@ -829,10 +841,7 @@ export async function executeQueryAll(elementLocator, params) {
|
|
|
829
841
|
const results = {};
|
|
830
842
|
|
|
831
843
|
for (const [name, selectorOrConfig] of Object.entries(params)) {
|
|
832
|
-
|
|
833
|
-
const queryParams = typeof selectorOrConfig === 'string'
|
|
834
|
-
? selectorOrConfig
|
|
835
|
-
: selectorOrConfig;
|
|
844
|
+
const queryParams = selectorOrConfig;
|
|
836
845
|
|
|
837
846
|
try {
|
|
838
847
|
results[name] = await executeQuery(elementLocator, queryParams);
|
|
@@ -457,13 +457,13 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
457
457
|
const eaParams = step.elementsAt;
|
|
458
458
|
if (Array.isArray(eaParams)) {
|
|
459
459
|
// Batch mode (array of coordinates)
|
|
460
|
-
stepResult.output = await executeElementsAt(pageController
|
|
460
|
+
stepResult.output = await executeElementsAt(pageController, eaParams);
|
|
461
461
|
} else if (eaParams && typeof eaParams === 'object' && eaParams.radius !== undefined) {
|
|
462
462
|
// Nearby mode (has radius)
|
|
463
|
-
stepResult.output = await executeElementsNear(pageController
|
|
463
|
+
stepResult.output = await executeElementsNear(pageController, eaParams);
|
|
464
464
|
} else {
|
|
465
465
|
// Single point mode (was refAt)
|
|
466
|
-
stepResult.output = await executeRefAt(pageController
|
|
466
|
+
stepResult.output = await executeRefAt(pageController, eaParams);
|
|
467
467
|
}
|
|
468
468
|
} else if (step.pageFunction !== undefined) {
|
|
469
469
|
stepResult.action = 'pageFunction';
|
|
@@ -576,7 +576,10 @@ export async function executeStep(deps, step, options = {}) {
|
|
|
576
576
|
|
|
577
577
|
if (isOptional) {
|
|
578
578
|
stepResult.status = 'skipped';
|
|
579
|
-
|
|
579
|
+
const isTimeout = error.code === 'TIMEOUT' || error.name === 'TimeoutError' || error.message?.includes('timed out');
|
|
580
|
+
stepResult.error = isTimeout
|
|
581
|
+
? `${error.message} (timeout: ${stepTimeout}ms)`
|
|
582
|
+
: error.message;
|
|
580
583
|
} else {
|
|
581
584
|
stepResult.status = 'error';
|
|
582
585
|
stepResult.error = error.message;
|
|
@@ -668,7 +671,11 @@ export async function runSteps(deps, steps, options = {}) {
|
|
|
668
671
|
result.navigated = navigated;
|
|
669
672
|
result.fullSnapshot = fullSnapshotPath;
|
|
670
673
|
result.context = afterContext;
|
|
671
|
-
|
|
674
|
+
|
|
675
|
+
// Write viewport snapshot to file to keep response concise
|
|
676
|
+
const viewportPath = await resolveTempPath(`${options.tabAlias || 'command'}.viewport.yaml`, '.yaml');
|
|
677
|
+
await fs.writeFile(viewportPath, afterViewport.yaml || '', 'utf8');
|
|
678
|
+
result.viewportSnapshot = viewportPath;
|
|
672
679
|
result.truncated = afterViewport.truncated || false;
|
|
673
680
|
|
|
674
681
|
if (!navigated && beforeViewport?.yaml) {
|
|
@@ -680,7 +680,7 @@ describe('step-executors goto profile integration', () => {
|
|
|
680
680
|
assert.strictEqual(result.action, 'goto');
|
|
681
681
|
assert.strictEqual(result.status, 'ok');
|
|
682
682
|
assert.ok(result.siteProfile);
|
|
683
|
-
assert.ok(result.siteProfile.
|
|
683
|
+
assert.ok(result.siteProfile.endsWith('.md'), 'siteProfile should be a file path');
|
|
684
684
|
});
|
|
685
685
|
|
|
686
686
|
it('should return profileAvailable false on goto for unknown domain', async () => {
|
|
@@ -501,8 +501,8 @@ describe('executeGetBox', () => {
|
|
|
501
501
|
|
|
502
502
|
describe('executeRefAt', () => {
|
|
503
503
|
it('should get element ref at coordinates', async () => {
|
|
504
|
-
const
|
|
505
|
-
const result = await executeRefAt(
|
|
504
|
+
const pageController = createMockPageController({ refAt: true });
|
|
505
|
+
const result = await executeRefAt(pageController, { x: 100, y: 200 });
|
|
506
506
|
|
|
507
507
|
assert.strictEqual(result.ref, 'f0s1e1');
|
|
508
508
|
assert.strictEqual(result.tag, 'BUTTON');
|
|
@@ -511,8 +511,8 @@ describe('executeRefAt', () => {
|
|
|
511
511
|
});
|
|
512
512
|
|
|
513
513
|
it('should return element info with box', async () => {
|
|
514
|
-
const
|
|
515
|
-
const result = await executeRefAt(
|
|
514
|
+
const pageController = createMockPageController({ refAt: true });
|
|
515
|
+
const result = await executeRefAt(pageController, { x: 100, y: 200 });
|
|
516
516
|
|
|
517
517
|
assert.ok(result.box);
|
|
518
518
|
assert.strictEqual(result.box.x, 100);
|
|
@@ -522,17 +522,17 @@ describe('executeRefAt', () => {
|
|
|
522
522
|
});
|
|
523
523
|
|
|
524
524
|
it('should throw if no element at coordinates', async () => {
|
|
525
|
-
const
|
|
525
|
+
const pageController = createMockPageController({ refAtNoElement: true });
|
|
526
526
|
await assert.rejects(
|
|
527
|
-
async () => executeRefAt(
|
|
527
|
+
async () => executeRefAt(pageController, { x: 999, y: 999 }),
|
|
528
528
|
/No element at coordinates/
|
|
529
529
|
);
|
|
530
530
|
});
|
|
531
531
|
|
|
532
532
|
it('should throw on evaluation error', async () => {
|
|
533
|
-
const
|
|
533
|
+
const pageController = createMockPageController({ evalError: true });
|
|
534
534
|
await assert.rejects(
|
|
535
|
-
async () => executeRefAt(
|
|
535
|
+
async () => executeRefAt(pageController, { x: 100, y: 100 }),
|
|
536
536
|
/Evaluation error/
|
|
537
537
|
);
|
|
538
538
|
});
|
|
@@ -544,12 +544,12 @@ describe('executeRefAt', () => {
|
|
|
544
544
|
|
|
545
545
|
describe('executeElementsAt', () => {
|
|
546
546
|
it('should get elements at multiple coordinates', async () => {
|
|
547
|
-
const
|
|
547
|
+
const pageController = createMockPageController({ elementsAt: true });
|
|
548
548
|
const coords = [
|
|
549
549
|
{ x: 100, y: 100 },
|
|
550
550
|
{ x: 200, y: 200 }
|
|
551
551
|
];
|
|
552
|
-
const result = await executeElementsAt(
|
|
552
|
+
const result = await executeElementsAt(pageController, coords);
|
|
553
553
|
|
|
554
554
|
assert.ok(result.results);
|
|
555
555
|
assert.strictEqual(result.results.length, 2);
|
|
@@ -558,16 +558,16 @@ describe('executeElementsAt', () => {
|
|
|
558
558
|
});
|
|
559
559
|
|
|
560
560
|
it('should handle empty coordinates array', async () => {
|
|
561
|
-
const
|
|
562
|
-
const result = await executeElementsAt(
|
|
561
|
+
const pageController = createMockPageController({ elementsAt: true });
|
|
562
|
+
const result = await executeElementsAt(pageController, []);
|
|
563
563
|
|
|
564
564
|
assert.ok(result.results || Array.isArray(result));
|
|
565
565
|
});
|
|
566
566
|
|
|
567
567
|
it('should throw on evaluation error', async () => {
|
|
568
|
-
const
|
|
568
|
+
const pageController = createMockPageController({ evalError: true });
|
|
569
569
|
await assert.rejects(
|
|
570
|
-
async () => executeElementsAt(
|
|
570
|
+
async () => executeElementsAt(pageController, [{ x: 100, y: 100 }]),
|
|
571
571
|
/Evaluation error/
|
|
572
572
|
);
|
|
573
573
|
});
|
|
@@ -579,9 +579,9 @@ describe('executeElementsAt', () => {
|
|
|
579
579
|
|
|
580
580
|
describe('executeElementsNear', () => {
|
|
581
581
|
it('should get elements near coordinates', async () => {
|
|
582
|
-
const
|
|
582
|
+
const pageController = createMockPageController({ elementsNear: true });
|
|
583
583
|
const params = { x: 100, y: 100, radius: 50 };
|
|
584
|
-
const result = await executeElementsNear(
|
|
584
|
+
const result = await executeElementsNear(pageController, params);
|
|
585
585
|
|
|
586
586
|
assert.ok(result.elements);
|
|
587
587
|
assert.strictEqual(result.elements.length, 2);
|
|
@@ -590,18 +590,18 @@ describe('executeElementsNear', () => {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
it('should default radius to 100 if not provided', async () => {
|
|
593
|
-
const
|
|
593
|
+
const pageController = createMockPageController({ elementsNear: true });
|
|
594
594
|
const params = { x: 100, y: 100 };
|
|
595
|
-
const result = await executeElementsNear(
|
|
595
|
+
const result = await executeElementsNear(pageController, params);
|
|
596
596
|
|
|
597
597
|
// Should still work with default radius
|
|
598
598
|
assert.ok(result.elements || result.searchRadius);
|
|
599
599
|
});
|
|
600
600
|
|
|
601
601
|
it('should throw on evaluation error', async () => {
|
|
602
|
-
const
|
|
602
|
+
const pageController = createMockPageController({ evalError: true });
|
|
603
603
|
await assert.rejects(
|
|
604
|
-
async () => executeElementsNear(
|
|
604
|
+
async () => executeElementsNear(pageController, { x: 100, y: 100 }),
|
|
605
605
|
/Evaluation error/
|
|
606
606
|
);
|
|
607
607
|
});
|