@ytspar/sweetlink 1.16.0 → 1.17.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/claude-skills/console-check-sweetlink/SKILL.md +1 -1
- package/claude-skills/console-check-sweetlink/evals/evals.json +35 -0
- package/claude-skills/resize-for-claude/evals/evals.json +23 -0
- package/claude-skills/responsive-screenshots/SKILL.md +1 -1
- package/claude-skills/responsive-screenshots/evals/evals.json +23 -0
- package/claude-skills/screenshot/SKILL.md +2 -0
- package/claude-skills/screenshot/evals/evals.json +35 -0
- package/dist/cli/sweetlink.js +70 -12
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts +10 -0
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js +57 -12
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/devices.d.ts +9 -4
- package/dist/daemon/devices.d.ts.map +1 -1
- package/dist/daemon/devices.js +25 -4
- package/dist/daemon/devices.js.map +1 -1
- package/dist/daemon/diff.d.ts +6 -0
- package/dist/daemon/diff.d.ts.map +1 -1
- package/dist/daemon/diff.js +46 -26
- package/dist/daemon/diff.js.map +1 -1
- package/dist/daemon/listeners.d.ts +2 -2
- package/dist/daemon/listeners.d.ts.map +1 -1
- package/dist/daemon/listeners.js +7 -5
- package/dist/daemon/listeners.js.map +1 -1
- package/dist/daemon/recording.d.ts +6 -1
- package/dist/daemon/recording.d.ts.map +1 -1
- package/dist/daemon/recording.js +81 -11
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/refs.d.ts.map +1 -1
- package/dist/daemon/refs.js +7 -2
- package/dist/daemon/refs.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +127 -12
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/summary.d.ts.map +1 -1
- package/dist/daemon/summary.js +52 -10
- package/dist/daemon/summary.js.map +1 -1
- package/dist/daemon/types.d.ts +1 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/daemon/viewer.d.ts +7 -0
- package/dist/daemon/viewer.d.ts.map +1 -1
- package/dist/daemon/viewer.js +12 -4
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/ruler.d.ts +1 -1
- package/dist/ruler.d.ts.map +1 -1
- package/dist/ruler.js +53 -32
- package/dist/ruler.js.map +1 -1
- package/package.json +37 -19
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: console-check-sweetlink
|
|
3
|
-
description:
|
|
3
|
+
description: "Check browser console for errors/warnings via Sweetlink WebSocket. Triggers on: \"console errors\", \"browser errors\", \"console check\", \"zero errors\". NOT for TypeScript/compile errors (use quick-typecheck) or server-side errors (use debugging agent)."
|
|
4
4
|
allowed-tools: Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "console-check-sweetlink",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Check the browser console for errors",
|
|
7
|
+
"expected_output": "The skill uses Sweetlink to connect to the browser and retrieve console logs",
|
|
8
|
+
"expectations": [
|
|
9
|
+
"Uses Sweetlink WebSocket or logs command to read browser console",
|
|
10
|
+
"Filters for errors and warnings",
|
|
11
|
+
"Reports any console errors found or confirms zero errors"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": 2,
|
|
16
|
+
"prompt": "Any console errors after my changes?",
|
|
17
|
+
"expected_output": "The skill checks the browser console for new errors via Sweetlink",
|
|
18
|
+
"expectations": [
|
|
19
|
+
"Triggers the console check flow via Sweetlink",
|
|
20
|
+
"Reports errors, warnings, or confirms clean console",
|
|
21
|
+
"Does not run TypeScript or lint checks"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 3,
|
|
26
|
+
"prompt": "Check for TypeScript errors",
|
|
27
|
+
"expected_output": "This should NOT trigger console-check-sweetlink. TypeScript errors are handled by quick-typecheck.",
|
|
28
|
+
"expectations": [
|
|
29
|
+
"Does NOT trigger console-check-sweetlink skill",
|
|
30
|
+
"Routes to quick-typecheck instead",
|
|
31
|
+
"No Sweetlink or browser commands are executed"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "resize-for-claude",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "This Figma export is 5120x11732 pixels, resize it so Claude can analyze it properly",
|
|
7
|
+
"expected_output": "Triggers resize-for-claude skill to scale the image down so the longest side is 1568px",
|
|
8
|
+
"expectations": ["Should trigger resize-for-claude skill", "Should run the resize-for-claude script", "Should detect tall aspect ratio and split into overlapping tiles if >= 3:1"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"prompt": "Optimize this full-page screenshot for Claude vision before I ask it to review the design",
|
|
13
|
+
"expected_output": "Triggers resize-for-claude skill to resize and potentially tile a tall screenshot",
|
|
14
|
+
"expectations": ["Should trigger resize-for-claude skill", "Should output to a -claude/ directory next to the original", "Should produce images with longest side <= 1568px"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 3,
|
|
18
|
+
"prompt": "Take a screenshot of the homepage at mobile, tablet, and desktop viewports",
|
|
19
|
+
"expected_output": "Should NOT trigger resize-for-claude - this is about capturing responsive screenshots, not resizing existing images",
|
|
20
|
+
"expectations": ["Should NOT trigger resize-for-claude - use responsive-screenshots for capturing at multiple viewports"]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: responsive-screenshots
|
|
3
|
-
description: Capture screenshots at
|
|
3
|
+
description: "Capture screenshots at mobile/tablet/desktop breakpoints via Sweetlink. Triggers on: \"responsive screenshots\", \"test breakpoints\", \"check mobile/tablet/desktop\", \"viewport screenshots\". NOT for single screenshots (use screenshot) or Playwright-based design review (use design-review agent)."
|
|
4
4
|
allowed-tools: Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "responsive-screenshots",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Capture responsive screenshots at mobile, tablet, and desktop to verify the new hero section",
|
|
7
|
+
"expected_output": "Triggers responsive-screenshots skill to capture at 375px, 768px, and 1440px viewports via Sweetlink",
|
|
8
|
+
"expectations": ["Should trigger responsive-screenshots skill", "Should capture at all three standard breakpoints (375, 768, 1440)", "Should save to .tmp/screenshots/ and review for responsive issues"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"prompt": "Test the breakpoints on the pricing page -- I want to see how it looks on mobile vs desktop",
|
|
13
|
+
"expected_output": "Triggers responsive-screenshots skill to capture the pricing page across viewports",
|
|
14
|
+
"expectations": ["Should trigger responsive-screenshots skill", "Should use pnpm sweetlink screenshot with --viewport flags", "Should analyze screenshots against the quality checklist"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 3,
|
|
18
|
+
"prompt": "Take a quick screenshot of the current page to verify my CSS change",
|
|
19
|
+
"expected_output": "Should NOT trigger responsive-screenshots - this is a single screenshot, not multi-viewport testing",
|
|
20
|
+
"expectations": ["Should NOT trigger responsive-screenshots - use screenshot skill for single screenshots"]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -84,6 +84,8 @@ pnpm sweetlink screenshot --viewport mobile --force-cdp --output .tmp/screenshot
|
|
|
84
84
|
|
|
85
85
|
### Agent-Browser (Any URL)
|
|
86
86
|
|
|
87
|
+
A live observability dashboard auto-opens at http://localhost:4848 on the user's first agent-browser command of a session (wired via PreToolUse hook in `~/.claude/settings.json`). Mention the URL when you start an agent-browser flow so the user can watch along.
|
|
88
|
+
|
|
87
89
|
```bash
|
|
88
90
|
# Open page and screenshot
|
|
89
91
|
agent-browser open http://localhost:3000
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "screenshot",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Take a screenshot of the app",
|
|
7
|
+
"expected_output": "The skill uses Sweetlink to capture a screenshot of the running application",
|
|
8
|
+
"expectations": [
|
|
9
|
+
"Uses Sweetlink screenshot command to capture the current page",
|
|
10
|
+
"Returns or saves the screenshot image",
|
|
11
|
+
"Does not use Playwright or other browser automation"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": 2,
|
|
16
|
+
"prompt": "Screenshot the homepage",
|
|
17
|
+
"expected_output": "The skill captures a screenshot of the specified page (homepage)",
|
|
18
|
+
"expectations": [
|
|
19
|
+
"Navigates to or targets the homepage URL/path",
|
|
20
|
+
"Uses Sweetlink screenshot command",
|
|
21
|
+
"Returns the captured screenshot for review"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 3,
|
|
26
|
+
"prompt": "Take screenshots at mobile, tablet, and desktop breakpoints",
|
|
27
|
+
"expected_output": "This should NOT trigger the single screenshot skill. Multi-viewport screenshots are handled by responsive-screenshots.",
|
|
28
|
+
"expectations": [
|
|
29
|
+
"Does NOT trigger screenshot skill for multi-viewport capture",
|
|
30
|
+
"Routes to responsive-screenshots instead",
|
|
31
|
+
"Single screenshot skill handles only one viewport at a time"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
package/dist/cli/sweetlink.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getCardHeaderPreset, getNavigationPreset, measureViaPlaywright } from '
|
|
|
14
14
|
import { DEFAULT_WS_PORT, MAX_PORT_RETRIES, WS_PORT_OFFSET } from '../types.js';
|
|
15
15
|
import { SCREENSHOT_DIR } from '../urlUtils.js';
|
|
16
16
|
import { daemonRequest, ensureDaemon, getDaemonStatus, stopDaemon } from '../daemon/client.js';
|
|
17
|
+
import { extractPort } from '../daemon/stateFile.js';
|
|
17
18
|
import { uploadEvidence } from '../daemon/evidence.js';
|
|
18
19
|
import { emitJson, printOutputSchema } from './outputSchemas.js';
|
|
19
20
|
const COMMON_APP_PORTS = [3000, 3001, 4000, 5173, 5174, 8000, 8080];
|
|
@@ -457,12 +458,25 @@ async function screenshot(options) {
|
|
|
457
458
|
selector: options.selector,
|
|
458
459
|
fullPage: options.fullPage,
|
|
459
460
|
viewport: options.viewport,
|
|
461
|
+
padding: options.padding,
|
|
460
462
|
});
|
|
461
463
|
const data = resp.data;
|
|
462
464
|
const outputPath = options.output || getDefaultScreenshotPath();
|
|
463
465
|
ensureDir(outputPath);
|
|
464
466
|
fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
|
|
465
467
|
reportScreenshotSuccess(outputPath, data.width, data.height, 'Daemon (hifi)', options.selector);
|
|
468
|
+
// UX: warn about silent .first() when multiple elements match.
|
|
469
|
+
if (options.selector && data.matchCount && data.matchCount > 1) {
|
|
470
|
+
console.warn(`[Sweetlink] ⚠ Selector '${options.selector}' matched ${data.matchCount} elements; captured the first. ` +
|
|
471
|
+
`Use --index N (with click) or a more specific selector to pick another.`);
|
|
472
|
+
}
|
|
473
|
+
// UX: hint at --full-page when content extends below the viewport.
|
|
474
|
+
if (!options.selector && !options.fullPage &&
|
|
475
|
+
data.pageHeight && data.viewportHeight && data.pageHeight > data.viewportHeight + 4) {
|
|
476
|
+
const overflow = data.pageHeight - data.viewportHeight;
|
|
477
|
+
console.log(`[Sweetlink] ℹ Page extends ${overflow}px below the viewport. ` +
|
|
478
|
+
`Use --full-page to capture all of it.`);
|
|
479
|
+
}
|
|
466
480
|
return {
|
|
467
481
|
path: getRelativePath(outputPath),
|
|
468
482
|
width: data.width,
|
|
@@ -2116,6 +2130,7 @@ async function handleStatusCommand() {
|
|
|
2116
2130
|
width: getArg('--width') ? parseInt(getArg('--width'), 10) : undefined,
|
|
2117
2131
|
height: getArg('--height') ? parseInt(getArg('--height'), 10) : undefined,
|
|
2118
2132
|
hover: hasFlag('--hover'),
|
|
2133
|
+
padding: getArg('--padding') ? parseInt(getArg('--padding'), 10) : undefined,
|
|
2119
2134
|
url: getArg('--url'),
|
|
2120
2135
|
wait: !hasFlag('--no-wait'), // Wait by default, --no-wait to skip
|
|
2121
2136
|
waitTimeout: getArg('--wait-timeout')
|
|
@@ -2180,22 +2195,50 @@ async function handleStatusCommand() {
|
|
|
2180
2195
|
}
|
|
2181
2196
|
case 'click': {
|
|
2182
2197
|
const clickTarget = getArg('--selector') ?? args[1];
|
|
2198
|
+
const clickText = getArg('--text');
|
|
2199
|
+
const clickIndex = getArg('--index') ? parseInt(getArg('--index'), 10) : 0;
|
|
2200
|
+
const projRoot = findProjectRoot();
|
|
2201
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2183
2202
|
// Route @e refs to daemon
|
|
2184
2203
|
if (clickTarget && /^@e\d+$/.test(clickTarget)) {
|
|
2185
|
-
const projRoot = findProjectRoot();
|
|
2186
|
-
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2187
2204
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2188
2205
|
await daemonRequest(state, 'click-ref', { ref: clickTarget });
|
|
2189
2206
|
console.log(`[Sweetlink] Clicked ${clickTarget}`);
|
|
2190
2207
|
result = { clicked: clickTarget, found: 1, index: 0 };
|
|
2208
|
+
break;
|
|
2191
2209
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2210
|
+
// If a recording is in progress, route CSS clicks through the daemon
|
|
2211
|
+
// so they target the recording page (which has no devbar/WebSocket
|
|
2212
|
+
// bridge) and get logged into the session manifest.
|
|
2213
|
+
try {
|
|
2214
|
+
const status = await getDaemonStatus(projRoot, extractPort(targetUrl));
|
|
2215
|
+
if (status.running) {
|
|
2216
|
+
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2217
|
+
const recStatus = await daemonRequest(state, 'record-status');
|
|
2218
|
+
const recData = recStatus.data;
|
|
2219
|
+
if (recData?.recording) {
|
|
2220
|
+
const resp = await daemonRequest(state, 'click-css', {
|
|
2221
|
+
selector: clickTarget,
|
|
2222
|
+
text: clickText,
|
|
2223
|
+
index: clickIndex,
|
|
2224
|
+
});
|
|
2225
|
+
const data = resp.data;
|
|
2226
|
+
console.log(`[Sweetlink] Clicked (recording): ${data.clicked ?? clickTarget ?? clickText}`);
|
|
2227
|
+
result = {
|
|
2228
|
+
clicked: data.clicked ?? 'unknown',
|
|
2229
|
+
found: data.found ?? 1,
|
|
2230
|
+
index: data.index ?? clickIndex,
|
|
2231
|
+
};
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2198
2235
|
}
|
|
2236
|
+
catch { /* fall through to WS path */ }
|
|
2237
|
+
result = await click({
|
|
2238
|
+
selector: clickTarget,
|
|
2239
|
+
text: clickText,
|
|
2240
|
+
index: clickIndex,
|
|
2241
|
+
});
|
|
2199
2242
|
break;
|
|
2200
2243
|
}
|
|
2201
2244
|
case 'network': {
|
|
@@ -2624,13 +2667,18 @@ async function handleStatusCommand() {
|
|
|
2624
2667
|
case 'daemon': {
|
|
2625
2668
|
const subcommand = args[1];
|
|
2626
2669
|
const projRoot = findProjectRoot();
|
|
2670
|
+
// Daemon state files are scoped by app port (`daemon-<port>.json`),
|
|
2671
|
+
// so honour --url for status/stop too — otherwise they look up the
|
|
2672
|
+
// un-suffixed `daemon.json` and miss the daemon that `start`
|
|
2673
|
+
// wrote with --url.
|
|
2674
|
+
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2675
|
+
const appPort = extractPort(targetUrl);
|
|
2627
2676
|
if (subcommand === 'stop') {
|
|
2628
|
-
const stopped = await stopDaemon(projRoot);
|
|
2677
|
+
const stopped = await stopDaemon(projRoot, appPort);
|
|
2629
2678
|
console.log(stopped ? '[Sweetlink] Daemon stopped.' : '[Sweetlink] No daemon running.');
|
|
2630
2679
|
result = { running: false };
|
|
2631
2680
|
}
|
|
2632
2681
|
else if (subcommand === 'start') {
|
|
2633
|
-
const targetUrl = getArg('--url') ?? 'http://localhost:3000';
|
|
2634
2682
|
const headedFlag = hasFlag('--headed');
|
|
2635
2683
|
const state = await ensureDaemon(projRoot, targetUrl, { headed: headedFlag });
|
|
2636
2684
|
console.log(`[Sweetlink] Daemon running on port ${state.port} (PID: ${state.pid})`);
|
|
@@ -2643,7 +2691,7 @@ async function handleStatusCommand() {
|
|
|
2643
2691
|
}
|
|
2644
2692
|
else {
|
|
2645
2693
|
// Default: status
|
|
2646
|
-
const status = await getDaemonStatus(projRoot);
|
|
2694
|
+
const status = await getDaemonStatus(projRoot, appPort);
|
|
2647
2695
|
if (status.running) {
|
|
2648
2696
|
console.log(`[Sweetlink] Daemon running: port=${status.port} pid=${status.pid} uptime=${status.uptime}s`);
|
|
2649
2697
|
}
|
|
@@ -2732,7 +2780,17 @@ async function handleStatusCommand() {
|
|
|
2732
2780
|
});
|
|
2733
2781
|
origExit(1);
|
|
2734
2782
|
}
|
|
2735
|
-
|
|
2783
|
+
// For Error objects, print just the message — the stack is rarely useful
|
|
2784
|
+
// to end users and clutters the output. Set SWEETLINK_DEBUG=1 to see it.
|
|
2785
|
+
if (error instanceof Error) {
|
|
2786
|
+
console.error(`[Sweetlink] ${error.message}`);
|
|
2787
|
+
if (process.env.SWEETLINK_DEBUG === '1' && error.stack) {
|
|
2788
|
+
console.error(error.stack);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
else {
|
|
2792
|
+
console.error('[Sweetlink] Fatal error:', error);
|
|
2793
|
+
}
|
|
2736
2794
|
process.exit(1);
|
|
2737
2795
|
}
|
|
2738
2796
|
})();
|