@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.
Files changed (50) hide show
  1. package/claude-skills/console-check-sweetlink/SKILL.md +1 -1
  2. package/claude-skills/console-check-sweetlink/evals/evals.json +35 -0
  3. package/claude-skills/resize-for-claude/evals/evals.json +23 -0
  4. package/claude-skills/responsive-screenshots/SKILL.md +1 -1
  5. package/claude-skills/responsive-screenshots/evals/evals.json +23 -0
  6. package/claude-skills/screenshot/SKILL.md +2 -0
  7. package/claude-skills/screenshot/evals/evals.json +35 -0
  8. package/dist/cli/sweetlink.js +70 -12
  9. package/dist/cli/sweetlink.js.map +1 -1
  10. package/dist/daemon/browser.d.ts +10 -0
  11. package/dist/daemon/browser.d.ts.map +1 -1
  12. package/dist/daemon/browser.js +57 -12
  13. package/dist/daemon/browser.js.map +1 -1
  14. package/dist/daemon/devices.d.ts +9 -4
  15. package/dist/daemon/devices.d.ts.map +1 -1
  16. package/dist/daemon/devices.js +25 -4
  17. package/dist/daemon/devices.js.map +1 -1
  18. package/dist/daemon/diff.d.ts +6 -0
  19. package/dist/daemon/diff.d.ts.map +1 -1
  20. package/dist/daemon/diff.js +46 -26
  21. package/dist/daemon/diff.js.map +1 -1
  22. package/dist/daemon/listeners.d.ts +2 -2
  23. package/dist/daemon/listeners.d.ts.map +1 -1
  24. package/dist/daemon/listeners.js +7 -5
  25. package/dist/daemon/listeners.js.map +1 -1
  26. package/dist/daemon/recording.d.ts +6 -1
  27. package/dist/daemon/recording.d.ts.map +1 -1
  28. package/dist/daemon/recording.js +81 -11
  29. package/dist/daemon/recording.js.map +1 -1
  30. package/dist/daemon/refs.d.ts.map +1 -1
  31. package/dist/daemon/refs.js +7 -2
  32. package/dist/daemon/refs.js.map +1 -1
  33. package/dist/daemon/server.d.ts.map +1 -1
  34. package/dist/daemon/server.js +127 -12
  35. package/dist/daemon/server.js.map +1 -1
  36. package/dist/daemon/summary.d.ts.map +1 -1
  37. package/dist/daemon/summary.js +52 -10
  38. package/dist/daemon/summary.js.map +1 -1
  39. package/dist/daemon/types.d.ts +1 -1
  40. package/dist/daemon/types.d.ts.map +1 -1
  41. package/dist/daemon/types.js.map +1 -1
  42. package/dist/daemon/viewer.d.ts +7 -0
  43. package/dist/daemon/viewer.d.ts.map +1 -1
  44. package/dist/daemon/viewer.js +12 -4
  45. package/dist/daemon/viewer.js.map +1 -1
  46. package/dist/ruler.d.ts +1 -1
  47. package/dist/ruler.d.ts.map +1 -1
  48. package/dist/ruler.js +53 -32
  49. package/dist/ruler.js.map +1 -1
  50. package/package.json +37 -19
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: console-check-sweetlink
3
- description: Quick console error check using Sweetlink. Use after making code changes, before marking tasks complete, during debugging, or when the user asks to check for console errors. Essential for the zero-error policy.
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 standard responsive breakpoints (mobile, tablet, desktop) to verify responsive design. Use when implementing responsive layouts, during design reviews, testing UI across viewports, or when the user asks to verify responsive behavior.
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
+ }
@@ -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
- else {
2193
- result = await click({
2194
- selector: clickTarget,
2195
- text: getArg('--text'),
2196
- index: getArg('--index') ? parseInt(getArg('--index'), 10) : undefined,
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
- console.error('[Sweetlink] Fatal error:', error);
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
  })();