@ytspar/sweetlink 1.14.0 → 1.15.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 (62) hide show
  1. package/README.md +207 -32
  2. package/claude-skills/screenshot/SKILL.md +21 -1
  3. package/dist/cli/sweetlink.js +295 -3
  4. package/dist/cli/sweetlink.js.map +1 -1
  5. package/dist/daemon/browser.d.ts +7 -0
  6. package/dist/daemon/browser.d.ts.map +1 -1
  7. package/dist/daemon/browser.js +21 -2
  8. package/dist/daemon/browser.js.map +1 -1
  9. package/dist/daemon/client.d.ts +5 -3
  10. package/dist/daemon/client.d.ts.map +1 -1
  11. package/dist/daemon/client.js +25 -19
  12. package/dist/daemon/client.js.map +1 -1
  13. package/dist/daemon/demo.d.ts +68 -0
  14. package/dist/daemon/demo.d.ts.map +1 -0
  15. package/dist/daemon/demo.js +247 -0
  16. package/dist/daemon/demo.js.map +1 -0
  17. package/dist/daemon/diff.js +5 -5
  18. package/dist/daemon/diff.js.map +1 -1
  19. package/dist/daemon/errorPatterns.d.ts +17 -0
  20. package/dist/daemon/errorPatterns.d.ts.map +1 -0
  21. package/dist/daemon/errorPatterns.js +81 -0
  22. package/dist/daemon/errorPatterns.js.map +1 -0
  23. package/dist/daemon/evidence.d.ts.map +1 -1
  24. package/dist/daemon/evidence.js +8 -7
  25. package/dist/daemon/evidence.js.map +1 -1
  26. package/dist/daemon/index.js +24 -12
  27. package/dist/daemon/index.js.map +1 -1
  28. package/dist/daemon/recording.d.ts +22 -12
  29. package/dist/daemon/recording.d.ts.map +1 -1
  30. package/dist/daemon/recording.js +114 -31
  31. package/dist/daemon/recording.js.map +1 -1
  32. package/dist/daemon/server.d.ts.map +1 -1
  33. package/dist/daemon/server.js +94 -17
  34. package/dist/daemon/server.js.map +1 -1
  35. package/dist/daemon/session.d.ts +6 -0
  36. package/dist/daemon/session.d.ts.map +1 -1
  37. package/dist/daemon/stateFile.d.ts +17 -10
  38. package/dist/daemon/stateFile.d.ts.map +1 -1
  39. package/dist/daemon/stateFile.js +34 -18
  40. package/dist/daemon/stateFile.js.map +1 -1
  41. package/dist/daemon/summary.d.ts +28 -0
  42. package/dist/daemon/summary.d.ts.map +1 -0
  43. package/dist/daemon/summary.js +152 -0
  44. package/dist/daemon/summary.js.map +1 -0
  45. package/dist/daemon/types.d.ts +1 -1
  46. package/dist/daemon/types.d.ts.map +1 -1
  47. package/dist/daemon/types.js.map +1 -1
  48. package/dist/daemon/viewer.d.ts +9 -18
  49. package/dist/daemon/viewer.d.ts.map +1 -1
  50. package/dist/daemon/viewer.js +441 -100
  51. package/dist/daemon/viewer.js.map +1 -1
  52. package/dist/server/index.d.ts.map +1 -1
  53. package/dist/server/index.js +173 -0
  54. package/dist/server/index.js.map +1 -1
  55. package/dist/types.d.ts +19 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/vite.d.ts +12 -0
  59. package/dist/vite.d.ts.map +1 -1
  60. package/dist/vite.js +13 -0
  61. package/dist/vite.js.map +1 -1
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -181,7 +181,7 @@ pnpm sweetlink query --selector "h1"
181
181
  ### Screenshots
182
182
 
183
183
  ```bash
184
- # Full page screenshot
184
+ # Full page screenshot (html2canvas, fast)
185
185
  pnpm sweetlink screenshot
186
186
 
187
187
  # Element screenshot
@@ -190,8 +190,23 @@ pnpm sweetlink screenshot --selector ".company-card"
190
190
  # Full page with custom output
191
191
  pnpm sweetlink screenshot --full-page --output page.png
192
192
 
193
- # Force CDP method (requires Chrome debugging)
194
- pnpm sweetlink screenshot --force-cdp
193
+ # Navigate to URL before capturing
194
+ pnpm sweetlink screenshot --url http://localhost:3000/about
195
+
196
+ # Custom viewport dimensions
197
+ pnpm sweetlink screenshot --force-cdp --width 375 --height 667
198
+ pnpm sweetlink screenshot --force-cdp --viewport mobile
199
+
200
+ # Pixel-perfect via daemon (see Persistent Daemon section)
201
+ pnpm sweetlink screenshot --hifi
202
+ pnpm sweetlink screenshot --responsive
203
+
204
+ # Skip server readiness check
205
+ pnpm sweetlink screenshot --no-wait
206
+
207
+ # Force specific method
208
+ pnpm sweetlink screenshot --force-cdp # Playwright/CDP
209
+ pnpm sweetlink screenshot --force-ws # WebSocket/html2canvas
195
210
  ```
196
211
 
197
212
  ### DOM Queries
@@ -306,30 +321,125 @@ pnpm sweetlink click --selector ".tab" --index 2
306
321
 
307
322
  **Debugging:** Use `DEBUG=1 pnpm sweetlink click ...` to see generated JavaScript.
308
323
 
309
- ### Network Requests (CDP Required)
324
+ ### Network Requests
310
325
 
311
326
  ```bash
312
- # Get all network requests
327
+ # Get all network requests (via WebSocket bridge)
313
328
  pnpm sweetlink network
314
329
 
315
330
  # Filter by URL
316
331
  pnpm sweetlink network --filter "/api/"
332
+
333
+ # Failed requests only (via daemon ring buffer)
334
+ pnpm sweetlink network --failed
335
+ pnpm sweetlink network --failed --last 10
336
+ ```
337
+
338
+ ### Schema, Outline, Accessibility
339
+
340
+ ```bash
341
+ # Page schema (JSON-LD, meta tags, Open Graph)
342
+ pnpm sweetlink schema
343
+
344
+ # Document outline (heading structure)
345
+ pnpm sweetlink outline
346
+
347
+ # Accessibility audit (axe-core)
348
+ pnpm sweetlink a11y
349
+
350
+ # Web Vitals (FCP, LCP, CLS, INP)
351
+ pnpm sweetlink vitals
352
+ ```
353
+
354
+ ### Server Management
355
+
356
+ ```bash
357
+ # Wait for dev server to be ready (useful in scripts)
358
+ pnpm sweetlink wait --url http://localhost:3000
359
+ pnpm sweetlink wait --url http://localhost:3000 --wait-timeout 60000
360
+
361
+ # Check server health
362
+ pnpm sweetlink status
363
+
364
+ # Clean up Sweetlink processes
365
+ pnpm sweetlink cleanup
366
+ pnpm sweetlink cleanup --force
367
+
368
+ # Target specific app in monorepo (by branch or app name)
369
+ pnpm sweetlink screenshot --app my-app
317
370
  ```
318
371
 
319
372
  ### Persistent Daemon (v2)
320
373
 
321
374
  Sweetlink v2 adds a persistent Playwright daemon for high-fidelity operations. The daemon auto-starts on first use and auto-stops after 30min idle.
322
375
 
376
+ #### Auto-Start with Vite
377
+
378
+ Add `daemon: true` to your Vite plugin config — the daemon starts/stops with your dev server:
379
+
380
+ ```typescript
381
+ // vite.config.ts
382
+ import { sweetlink } from '@ytspar/sweetlink/vite';
383
+
384
+ export default defineConfig({
385
+ plugins: [sweetlink({ daemon: true })]
386
+ // Or with visible browser: sweetlink({ daemon: true, headed: true })
387
+ });
388
+ ```
389
+
390
+ #### Auto-Start with Dev Script
391
+
392
+ Add a `dev` script that starts both your app and the daemon:
393
+
394
+ ```json
395
+ {
396
+ "scripts": {
397
+ "dev": "vite",
398
+ "dev:daemon": "concurrently 'vite' 'sweetlink daemon start --url http://localhost:5173'"
399
+ }
400
+ }
401
+ ```
402
+
403
+ #### Multiple Apps (Monorepo)
404
+
405
+ Each daemon is scoped by app port. Running two apps on different ports creates separate daemons:
406
+
407
+ ```bash
408
+ # Terminal 1: App A on port 3000
409
+ sweetlink daemon start --url http://localhost:3000
410
+ # State: .sweetlink/daemon-3000.json
411
+
412
+ # Terminal 2: App B on port 5173
413
+ sweetlink daemon start --url http://localhost:5173
414
+ # State: .sweetlink/daemon-5173.json
415
+
416
+ # Commands target specific apps via --url
417
+ sweetlink screenshot --hifi --url http://localhost:3000
418
+ sweetlink screenshot --hifi --url http://localhost:5173
419
+ ```
420
+
421
+ #### Manual Lifecycle
422
+
423
+ ```bash
424
+ # Start manually (auto-starts on first --hifi command if not running)
425
+ pnpm sweetlink daemon start --url http://localhost:5173
426
+ pnpm sweetlink daemon start --url http://localhost:5173 --headed # visible browser
427
+
428
+ # Check status
429
+ pnpm sweetlink daemon status
430
+
431
+ # Stop (or wait for 30min idle auto-stop)
432
+ pnpm sweetlink daemon stop
433
+ ```
434
+
435
+ #### CLI Quick Reference
436
+
323
437
  ```bash
324
438
  # Pixel-perfect screenshot via persistent daemon (~150ms after startup)
325
439
  pnpm sweetlink screenshot --hifi
326
440
 
327
441
  # Responsive screenshots at 3 breakpoints (375/768/1280)
328
442
  pnpm sweetlink screenshot --responsive
329
-
330
- # Manage daemon lifecycle
331
- pnpm sweetlink daemon status
332
- pnpm sweetlink daemon stop
333
443
  ```
334
444
 
335
445
  ### Accessibility Snapshots & Refs
@@ -375,6 +485,38 @@ pnpm sweetlink record stop
375
485
  # Generates .sweetlink/<session-id>/viewer.html
376
486
  ```
377
487
 
488
+ ### Demo Documents
489
+
490
+ Build a Markdown tutorial/proof document step-by-step (inspired by [Showboat](https://simonwillison.net/2026/Feb/10/showboat-and-rodney/)):
491
+
492
+ ```bash
493
+ # Start a new demo
494
+ pnpm sweetlink demo init "How to use the search feature"
495
+
496
+ # Add narrative prose
497
+ pnpm sweetlink demo note "First, run the tests to see them pass."
498
+
499
+ # Run a command and capture output inline
500
+ pnpm sweetlink demo exec "pnpm test -- --grep search"
501
+
502
+ # Take a screenshot and embed it
503
+ pnpm sweetlink demo screenshot --url http://localhost:5173 --caption "Search results"
504
+
505
+ # Capture accessibility tree
506
+ pnpm sweetlink demo snapshot --url http://localhost:5173
507
+
508
+ # Remove last section if it's wrong
509
+ pnpm sweetlink demo pop
510
+
511
+ # Re-run all commands to verify outputs haven't changed
512
+ pnpm sweetlink demo verify
513
+
514
+ # Check status
515
+ pnpm sweetlink demo status
516
+ ```
517
+
518
+ Result: a `DEMO.md` with embedded command outputs and screenshots that serves as both documentation and a regression test.
519
+
378
520
  ### PR Evidence
379
521
 
380
522
  ```bash
@@ -564,19 +706,53 @@ This 15x token savings enables 10+ autonomous iterations within Claude's budget.
564
706
 
565
707
  ## Comparison with Alternatives
566
708
 
567
- | Feature | Sweetlink | Playwright MCP | Manual Screenshots |
568
- |---------|-----------|----------------|-------------------|
569
- | Setup Time | < 1 min | 5-10 min | N/A |
570
- | Token Cost | ~1,000 | ~5,000 | N/A |
571
- | Auto Reconnect | | | N/A |
572
- | Console Logs | | | |
573
- | Network Requests | ✅ (CDP) | | ❌ |
574
- | DOM Queries | ✅ | ✅ | |
575
- | JS Execution | ✅ | | ❌ |
576
- | Click Elements | ✅ | ✅ | ❌ |
577
- | Element Screenshots | ✅ | | ❌ |
578
- | Full Page Screenshots | ✅ | ✅ | ✅ |
579
- | Autonomous Loops | ✅ (10+) | Limited (2-3) | ❌ |
709
+ ### Screenshot & Interaction
710
+
711
+ | Feature | Sweetlink | Playwright CLI | Chrome DevTools MCP | agent-browser |
712
+ |---------|-----------|----------------|--------------------|--------------|
713
+ | Setup | Vite/Next plugin | npm install | Chrome flag | npm install |
714
+ | Token cost/screenshot | ~1,000 | ~5,000 | ~5,000 | ~3,000 |
715
+ | Fast screenshots (html2canvas) | ✅ | | | ❌ |
716
+ | Pixel-perfect screenshots | ✅ (`--hifi`) | ✅ | | ✅ |
717
+ | Responsive (multi-viewport) | ✅ (`--responsive`) | Manual | ❌ | ❌ |
718
+ | Device emulation (iPhone, Pixel) | ✅ (named presets) | ✅ | ❌ | ✅ |
719
+ | @ref element interaction | ✅ (accessibility tree) | (CSS selectors) | ❌ | ✅ (Stagehand AI) |
720
+ | DOM queries | ✅ | ✅ | ✅ | ❌ |
721
+ | JS execution | ✅ | | | ❌ |
722
+ | Persistent browser sessions | ✅ (daemon) | ❌ (per-run) | ✅ | ✅ |
723
+
724
+ ### Evidence & Reporting
725
+
726
+ | Feature | Sweetlink | ProofShot | Proof | Playwright |
727
+ |---------|-----------|-----------|-------|------------|
728
+ | Video recording (WebM) | ✅ | ✅ | ✅ | ✅ (trace) |
729
+ | Interactive HTML viewer | ✅ (video + overlays) | ✅ | ✅ | ✅ (trace viewer) |
730
+ | Click ripple overlays | ✅ (canvas) | ✅ | ✅ (FFmpeg) | ❌ |
731
+ | Console ring buffer | ✅ (always-on, 50K) | ✅ | ❌ | ❌ |
732
+ | Network ring buffer | ✅ (always-on, 50K) | ❌ | ❌ | ✅ (HAR) |
733
+ | Multi-language error detection | ✅ (10+ languages) | ✅ | ❌ | ❌ |
734
+ | Snapshot diffing (a11y tree) | ✅ | ❌ | ❌ | ❌ |
735
+ | Annotated screenshots | ✅ (@ref labels) | ✅ | ❌ | ❌ |
736
+ | SUMMARY.md report | ✅ | ✅ | ✅ (3 formats) | ❌ |
737
+ | Demo documents (Showboat-style) | ✅ | ❌ | ❌ | ❌ |
738
+ | PR evidence upload | ✅ (`proof --pr`) | ✅ | ❌ | ❌ |
739
+ | Webhook sharing | ✅ (any URL) | ❌ | ❌ | ❌ |
740
+ | Self-contained viewer (offline) | ✅ (base64 video) | ✅ | ✅ | ✅ |
741
+
742
+ ### Developer Experience
743
+
744
+ | Feature | Sweetlink | ProofShot | Chrome DevTools MCP |
745
+ |---------|-----------|-----------|---------------------|
746
+ | Devbar toolbar UI | ✅ | ❌ | ❌ |
747
+ | Record button in browser | ✅ | ❌ | ❌ |
748
+ | Viewer auto-opens on stop | ✅ | ❌ | ❌ |
749
+ | Copy Report to clipboard | ✅ (CLI + viewer) | ❌ | ❌ |
750
+ | Serve viewer on network | ✅ (`report --serve`) | ❌ | ❌ |
751
+ | Multi-instance (monorepo) | ✅ (per-port daemon) | ❌ | ❌ |
752
+ | Vite/Next.js auto-start | ✅ (plugin option) | ❌ | ❌ |
753
+ | Accessibility audit (axe-core) | ✅ | ❌ | ❌ |
754
+ | Web Vitals | ✅ | ❌ | ❌ |
755
+ | Ruler/measurement tool | ✅ | ❌ | ❌ |
580
756
 
581
757
  ## When to Use Alternatives
582
758
 
@@ -585,22 +761,21 @@ This 15x token savings enables 10+ autonomous iterations within Claude's budget.
585
761
  **Use Sweetlink when:**
586
762
  - You're debugging/iterating on a running dev server
587
763
  - You need lightweight, token-efficient screenshots (~1000 tokens vs ~5000)
588
- - You want real-time console log capture
589
- - Your app is already running and you just need to inspect it
764
+ - You want always-on console/network capture without "start watching"
765
+ - You need session recording with interactive video viewer
766
+ - You want demo documents that serve as both tutorials and regression tests
590
767
  - You're doing autonomous UI development loops (10+ iterations)
768
+ - You want a toolbar UI in the browser for visual debugging
591
769
 
592
770
  **Use [Agent Browser](https://github.com/vercel-labs/agent-browser) when:**
593
- - You need full browser automation (navigation, form filling, multi-page flows)
594
- - You're testing production sites or external URLs
595
- - Sweetlink isn't integrated into the target application
771
+ - You're testing production sites or external URLs where Sweetlink isn't installed
596
772
  - You need Stagehand's AI-powered element selection
597
773
  - You're building autonomous agents that interact with arbitrary websites
598
774
 
599
- **Use Playwright MCP when:**
600
- - You need precise, programmatic browser control
601
- - You're running E2E tests with assertions
602
- - You need browser contexts, multiple tabs, or complex scenarios
603
- - You require network interception or request mocking
775
+ **Use Playwright directly when:**
776
+ - You're writing structured E2E test suites with assertions
777
+ - You need network interception, request mocking, or multiple browser contexts
778
+ - You want trace viewer for debugging test failures
604
779
 
605
780
  ### Agent Browser Quick Start
606
781
 
@@ -27,7 +27,7 @@ CLI (pnpm sweetlink)
27
27
  console, network --failed, record, proof
28
28
  ```
29
29
 
30
- The daemon auto-starts on first `--hifi`/`snapshot` command and auto-stops after 30min idle. State file at `.sweetlink/daemon.json`.
30
+ The daemon auto-starts on first `--hifi`/`snapshot` command and auto-stops after 30min idle. State file at `.sweetlink/daemon-{port}.json` (scoped per app port for multi-instance support).
31
31
 
32
32
  ### Prerequisites
33
33
 
@@ -344,9 +344,29 @@ pnpm sweetlink record status
344
344
  ```bash
345
345
  pnpm sweetlink daemon status
346
346
  pnpm sweetlink daemon start --url http://localhost:3000
347
+ pnpm sweetlink daemon start --url http://localhost:3000 --headed # visible browser
347
348
  pnpm sweetlink daemon stop
348
349
  ```
349
350
 
351
+ ### proof — Upload session evidence to GitHub PR
352
+
353
+ ```bash
354
+ pnpm sweetlink proof --pr 123
355
+ pnpm sweetlink proof --pr 123 --repo owner/repo
356
+ ```
357
+
358
+ ### Other useful commands
359
+
360
+ ```bash
361
+ pnpm sweetlink wait --url http://localhost:3000 # Wait for server ready
362
+ pnpm sweetlink status # Check server health
363
+ pnpm sweetlink vitals # Get Web Vitals
364
+ pnpm sweetlink a11y # Accessibility audit
365
+ pnpm sweetlink schema # Page schema (meta, OG, JSON-LD)
366
+ pnpm sweetlink outline # Document heading structure
367
+ pnpm sweetlink cleanup --force # Clean up Sweetlink processes
368
+ ```
369
+
350
370
  ## Agent-Browser Commands
351
371
 
352
372
  ### Basic Usage
@@ -1843,6 +1843,49 @@ const COMMAND_HELP = {
1843
1843
  pnpm sweetlink snapshot -i
1844
1844
  pnpm sweetlink click @e3
1845
1845
  pnpm sweetlink record stop`,
1846
+ report: ` report [options]
1847
+ Print or share the latest session report.
1848
+
1849
+ Modes:
1850
+ (default) Print SUMMARY.md to stdout
1851
+ --clipboard Copy SUMMARY.md to clipboard (pbcopy on macOS, xclip on Linux)
1852
+ --serve Start a temporary HTTP server serving viewer.html
1853
+ --webhook <url> POST session data as JSON to a webhook URL
1854
+
1855
+ Options:
1856
+ --session <dir> Session directory (default: .sweetlink)
1857
+
1858
+ Examples:
1859
+ pnpm sweetlink report
1860
+ pnpm sweetlink report --clipboard
1861
+ pnpm sweetlink report --serve
1862
+ pnpm sweetlink report --webhook https://hooks.slack.com/...`,
1863
+ demo: ` demo <subcommand> [options]
1864
+ Build a Markdown demo document step-by-step.
1865
+ Each command appends a section. The result is a reproducible
1866
+ tutorial/proof document with embedded outputs and screenshots.
1867
+
1868
+ Subcommands:
1869
+ init <title> Start a new demo document
1870
+ note <text> Add a prose note section
1871
+ exec <command> Run command, capture output inline
1872
+ screenshot [--caption] Take hifi screenshot and embed
1873
+ snapshot Capture accessibility tree inline
1874
+ pop Remove the last section
1875
+ verify Re-run all exec sections and verify outputs
1876
+ status Show current demo state
1877
+
1878
+ Options:
1879
+ --url <url> Dev server URL (for screenshots/snapshots)
1880
+ --output <dir> Output directory (default: .sweetlink/demo)
1881
+ --caption <text> Caption for screenshot
1882
+
1883
+ Examples:
1884
+ pnpm sweetlink demo init "How to use the search feature"
1885
+ pnpm sweetlink demo note "First, navigate to the search page."
1886
+ pnpm sweetlink demo exec "pnpm test -- --grep search"
1887
+ pnpm sweetlink demo screenshot --caption "Search results"
1888
+ pnpm sweetlink demo verify`,
1846
1889
  };
1847
1890
  // Aliases that map to canonical command names
1848
1891
  const COMMAND_ALIASES = {
@@ -1947,6 +1990,8 @@ if (hasFlag('--output-schema')) {
1947
1990
  'console',
1948
1991
  'record',
1949
1992
  'proof',
1993
+ 'report',
1994
+ 'demo',
1950
1995
  ];
1951
1996
  const schemaCommand = knownCommands.includes(commandType)
1952
1997
  ? commandType === 'measure'
@@ -2313,8 +2358,23 @@ async function handleStatusCommand() {
2313
2358
  else if (subcommand === 'stop') {
2314
2359
  const resp = await daemonRequest(state, 'record-stop');
2315
2360
  const data = resp.data;
2316
- console.log('[Sweetlink] Recording stopped.');
2317
- console.log(JSON.stringify(data.manifest, null, 2));
2361
+ const m = data.manifest;
2362
+ console.log(`[Sweetlink] Recording stopped: ${m.sessionId}`);
2363
+ console.log(` Duration: ${m.duration.toFixed(1)}s | Actions: ${m.commands.length}${m.video ? ' | Video: ' + m.video : ''}`);
2364
+ // Auto-open the viewer
2365
+ if (data.viewerPath && !hasFlag('--no-open')) {
2366
+ console.log(` Viewer: ${data.viewerPath}`);
2367
+ const { execFile } = await import('child_process');
2368
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
2369
+ execFile(openCmd, [data.viewerPath], (err) => {
2370
+ if (err)
2371
+ console.error(' Could not open viewer:', err.message);
2372
+ });
2373
+ console.log(` Opened in browser.`);
2374
+ }
2375
+ else if (data.viewerPath) {
2376
+ console.log(` Viewer: ${data.viewerPath}`);
2377
+ }
2318
2378
  result = data;
2319
2379
  }
2320
2380
  else {
@@ -2330,6 +2390,237 @@ async function handleStatusCommand() {
2330
2390
  }
2331
2391
  break;
2332
2392
  }
2393
+ case 'report': {
2394
+ const sessionDirArg = getArg('--session') ?? '.sweetlink';
2395
+ // Find latest session directory
2396
+ if (!fs.existsSync(sessionDirArg)) {
2397
+ console.error(`[Sweetlink] Session directory not found: ${sessionDirArg}`);
2398
+ process.exit(1);
2399
+ }
2400
+ const reportSessions = fs.readdirSync(sessionDirArg)
2401
+ .filter((f) => f.startsWith('session-'))
2402
+ .sort()
2403
+ .reverse();
2404
+ if (reportSessions.length === 0) {
2405
+ console.error('[Sweetlink] No session found. Run `record start` and `record stop` first.');
2406
+ process.exit(1);
2407
+ }
2408
+ const reportSessionDir = path.join(sessionDirArg, reportSessions[0]);
2409
+ if (hasFlag('--clipboard')) {
2410
+ // Copy SUMMARY.md to clipboard
2411
+ const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
2412
+ if (!fs.existsSync(summaryPath)) {
2413
+ console.error(`[Sweetlink] SUMMARY.md not found at ${summaryPath}`);
2414
+ process.exit(1);
2415
+ }
2416
+ const summaryContent = fs.readFileSync(summaryPath, 'utf-8');
2417
+ const { execFileSync } = await import('child_process');
2418
+ const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip';
2419
+ const clipArgs = process.platform === 'darwin' ? [] : ['-selection', 'clipboard'];
2420
+ try {
2421
+ execFileSync(clipCmd, clipArgs, { input: summaryContent });
2422
+ console.log('[Sweetlink] SUMMARY.md copied to clipboard.');
2423
+ }
2424
+ catch (err) {
2425
+ console.error(`[Sweetlink] Failed to copy to clipboard (${clipCmd}):`, err instanceof Error ? err.message : err);
2426
+ process.exit(1);
2427
+ }
2428
+ result = { mode: 'clipboard', session: reportSessions[0] };
2429
+ }
2430
+ else if (hasFlag('--serve')) {
2431
+ // Serve viewer.html on a random port
2432
+ const viewerPath = path.join(reportSessionDir, 'viewer.html');
2433
+ if (!fs.existsSync(viewerPath)) {
2434
+ console.error(`[Sweetlink] viewer.html not found at ${viewerPath}`);
2435
+ process.exit(1);
2436
+ }
2437
+ const viewerContent = fs.readFileSync(viewerPath, 'utf-8');
2438
+ const http = await import('http');
2439
+ const port = 10000 + Math.floor(Math.random() * 50000);
2440
+ const server = http.createServer((_req, res) => {
2441
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2442
+ res.end(viewerContent);
2443
+ });
2444
+ server.listen(port, '0.0.0.0', () => {
2445
+ const os = require('os');
2446
+ const nets = os.networkInterfaces();
2447
+ let lanIp = 'localhost';
2448
+ for (const name of Object.keys(nets)) {
2449
+ for (const net of nets[name]) {
2450
+ if (net.family === 'IPv4' && !net.internal) {
2451
+ lanIp = net.address;
2452
+ break;
2453
+ }
2454
+ }
2455
+ if (lanIp !== 'localhost')
2456
+ break;
2457
+ }
2458
+ console.log(`[Sweetlink] Serving viewer at:`);
2459
+ console.log(` Local: http://localhost:${port}`);
2460
+ console.log(` Network: http://${lanIp}:${port}`);
2461
+ console.log(' Press Ctrl+C to stop.');
2462
+ });
2463
+ // Keep running until Ctrl+C
2464
+ await new Promise(() => { });
2465
+ }
2466
+ else if (getArg('--webhook')) {
2467
+ // POST session data to webhook
2468
+ const webhookUrl = getArg('--webhook');
2469
+ const manifestPath = path.join(reportSessionDir, 'sweetlink-session.json');
2470
+ const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
2471
+ if (!fs.existsSync(manifestPath)) {
2472
+ console.error(`[Sweetlink] Manifest not found at ${manifestPath}`);
2473
+ process.exit(1);
2474
+ }
2475
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
2476
+ const summary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf-8') : '';
2477
+ const payload = { summary, manifest };
2478
+ // Include viewer HTML for Slack/Discord webhooks
2479
+ if (/slack|discord/i.test(webhookUrl)) {
2480
+ const viewerPath = path.join(reportSessionDir, 'viewer.html');
2481
+ if (fs.existsSync(viewerPath)) {
2482
+ payload.viewerHtml = fs.readFileSync(viewerPath, 'utf-8');
2483
+ }
2484
+ }
2485
+ const body = JSON.stringify(payload);
2486
+ const res = await fetch(webhookUrl, {
2487
+ method: 'POST',
2488
+ headers: { 'Content-Type': 'application/json' },
2489
+ body,
2490
+ });
2491
+ if (res.ok) {
2492
+ console.log(`[Sweetlink] Report posted to ${webhookUrl} (${res.status})`);
2493
+ }
2494
+ else {
2495
+ console.error(`[Sweetlink] Webhook failed: ${res.status} ${res.statusText}`);
2496
+ process.exit(1);
2497
+ }
2498
+ result = { mode: 'webhook', url: webhookUrl, status: res.status };
2499
+ }
2500
+ else {
2501
+ // Default: print SUMMARY.md to stdout
2502
+ const summaryPath = path.join(reportSessionDir, 'SUMMARY.md');
2503
+ if (!fs.existsSync(summaryPath)) {
2504
+ console.error(`[Sweetlink] SUMMARY.md not found at ${summaryPath}`);
2505
+ process.exit(1);
2506
+ }
2507
+ const summaryContent = fs.readFileSync(summaryPath, 'utf-8');
2508
+ process.stdout.write(summaryContent);
2509
+ result = { mode: 'stdout', session: reportSessions[0] };
2510
+ }
2511
+ break;
2512
+ }
2513
+ case 'demo': {
2514
+ const sub = args[1];
2515
+ const projRoot = findProjectRoot();
2516
+ const demoDir = getArg('--output') ?? path.join(projRoot, '.sweetlink', 'demo');
2517
+ const stateFile = path.join(demoDir, 'demo-state.json');
2518
+ // Lazy import demo module
2519
+ const demoMod = await import('../daemon/demo.js');
2520
+ if (sub === 'init') {
2521
+ const title = args[2];
2522
+ if (!title) {
2523
+ console.error('[Sweetlink] Error: demo init requires a title');
2524
+ process.exit(1);
2525
+ }
2526
+ const demoState = await demoMod.initDemo(title, demoDir, { url: getArg('--url') });
2527
+ await demoMod.writeDemo(demoState);
2528
+ console.log(`[Sweetlink] Demo initialized: ${demoState.filePath}`);
2529
+ result = { filePath: demoState.filePath };
2530
+ }
2531
+ else if (sub === 'note') {
2532
+ const text = args.slice(2).join(' ');
2533
+ if (!text) {
2534
+ console.error('[Sweetlink] Error: demo note requires text');
2535
+ process.exit(1);
2536
+ }
2537
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2538
+ const updated = demoMod.addNote(state, text);
2539
+ await demoMod.writeDemo(updated);
2540
+ console.log(`[Sweetlink] Note added (${updated.sections.length} sections)`);
2541
+ result = { sections: updated.sections.length };
2542
+ }
2543
+ else if (sub === 'exec') {
2544
+ const cmd = args.slice(2).join(' ');
2545
+ if (!cmd) {
2546
+ console.error('[Sweetlink] Error: demo exec requires a command');
2547
+ process.exit(1);
2548
+ }
2549
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2550
+ const updated = await demoMod.addExec(state, cmd, []);
2551
+ await demoMod.writeDemo(updated);
2552
+ const lastSection = updated.sections[updated.sections.length - 1];
2553
+ console.log(`[Sweetlink] Exec added: ${cmd} (exit ${lastSection.exitCode ?? 0})`);
2554
+ result = { sections: updated.sections.length, exitCode: lastSection.exitCode };
2555
+ }
2556
+ else if (sub === 'screenshot') {
2557
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2558
+ const caption = getArg('--caption') ?? 'Screenshot';
2559
+ const daemonState = await ensureDaemon(projRoot, targetUrl);
2560
+ const resp = await daemonRequest(daemonState, 'screenshot', {});
2561
+ const data = resp.data;
2562
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2563
+ const updated = await demoMod.addScreenshot(state, Buffer.from(data.screenshot, 'base64'), caption);
2564
+ await demoMod.writeDemo(updated);
2565
+ console.log(`[Sweetlink] Screenshot added: ${caption}`);
2566
+ result = { sections: updated.sections.length };
2567
+ }
2568
+ else if (sub === 'snapshot') {
2569
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2570
+ const daemonState = await ensureDaemon(projRoot, targetUrl);
2571
+ const resp = await daemonRequest(daemonState, 'snapshot', { interactive: true });
2572
+ const data = resp.data;
2573
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2574
+ const updated = demoMod.addSnapshot(state, data.tree);
2575
+ await demoMod.writeDemo(updated);
2576
+ console.log(`[Sweetlink] Snapshot added (${updated.sections.length} sections)`);
2577
+ result = { sections: updated.sections.length };
2578
+ }
2579
+ else if (sub === 'pop') {
2580
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2581
+ const updated = demoMod.popSection(state);
2582
+ await demoMod.writeDemo(updated);
2583
+ console.log(`[Sweetlink] Last section removed (${updated.sections.length} remaining)`);
2584
+ result = { sections: updated.sections.length };
2585
+ }
2586
+ else if (sub === 'verify') {
2587
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2588
+ const verifyResult = await demoMod.verifyDemo(state);
2589
+ if (verifyResult.passed) {
2590
+ console.log('[Sweetlink] Demo verified: all outputs match');
2591
+ }
2592
+ else {
2593
+ console.log(`[Sweetlink] Demo verification FAILED: ${verifyResult.failures.length} mismatch(es)`);
2594
+ for (const f of verifyResult.failures) {
2595
+ console.log(` Section ${f.index}: ${f.command}`);
2596
+ console.log(` Expected: ${f.expected.substring(0, 80)}...`);
2597
+ console.log(` Actual: ${f.actual.substring(0, 80)}...`);
2598
+ }
2599
+ }
2600
+ result = verifyResult;
2601
+ }
2602
+ else {
2603
+ // status
2604
+ if (fs.existsSync(stateFile)) {
2605
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
2606
+ console.log(`[Sweetlink] Demo: "${state.title}" (${state.sections.length} sections)`);
2607
+ console.log(` File: ${state.filePath}`);
2608
+ for (const s of state.sections) {
2609
+ const preview = s.type === 'note' ? s.content.substring(0, 60) :
2610
+ s.type === 'exec' ? `$ ${s.command}` :
2611
+ s.type === 'screenshot' ? `[image] ${s.screenshotFile}` :
2612
+ '[snapshot]';
2613
+ console.log(` ${s.type.padEnd(12)} ${preview}`);
2614
+ }
2615
+ result = state;
2616
+ }
2617
+ else {
2618
+ console.log('[Sweetlink] No demo in progress. Run `demo init <title>` to start.');
2619
+ result = null;
2620
+ }
2621
+ }
2622
+ break;
2623
+ }
2333
2624
  case 'daemon': {
2334
2625
  const subcommand = args[1];
2335
2626
  const projRoot = findProjectRoot();
@@ -2340,7 +2631,8 @@ async function handleStatusCommand() {
2340
2631
  }
2341
2632
  else if (subcommand === 'start') {
2342
2633
  const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2343
- const state = await ensureDaemon(projRoot, targetUrl);
2634
+ const headedFlag = hasFlag('--headed');
2635
+ const state = await ensureDaemon(projRoot, targetUrl, { headed: headedFlag });
2344
2636
  console.log(`[Sweetlink] Daemon running on port ${state.port} (PID: ${state.pid})`);
2345
2637
  result = {
2346
2638
  running: true,