@rubytech/create-maxy-code 0.1.185 → 0.1.187

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 (61) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +70 -0
  3. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +52 -0
  4. package/payload/platform/plugins/admin/skills/a4-print-documents/SKILL.md +14 -14
  5. package/payload/platform/plugins/admin/skills/deck-pages/SKILL.md +9 -23
  6. package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +1 -1
  7. package/payload/platform/plugins/admin/skills/skill-builder/SKILL.md +1 -1
  8. package/payload/platform/plugins/admin/skills/skill-builder/references/pdf-generation.md +4 -4
  9. package/payload/platform/plugins/browser/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/browser/PLUGIN.md +78 -8
  11. package/payload/platform/plugins/browser/mcp/dist/index.js +111 -7
  12. package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-actions.d.ts +98 -0
  14. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-actions.d.ts.map +1 -0
  15. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-actions.js +455 -0
  16. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-actions.js.map +1 -0
  17. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts +9 -31
  18. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts.map +1 -1
  19. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js +6 -165
  20. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js.map +1 -1
  21. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-session.d.ts +153 -0
  22. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-session.d.ts.map +1 -0
  23. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-session.js +401 -0
  24. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-session.js.map +1 -0
  25. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  26. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  27. package/payload/platform/plugins/email/PLUGIN.md +6 -2
  28. package/payload/platform/plugins/email/mcp/dist/index.js +38 -0
  29. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/email/mcp/dist/lib/providers.d.ts +28 -0
  31. package/payload/platform/plugins/email/mcp/dist/lib/providers.d.ts.map +1 -1
  32. package/payload/platform/plugins/email/mcp/dist/lib/providers.js +106 -0
  33. package/payload/platform/plugins/email/mcp/dist/lib/providers.js.map +1 -1
  34. package/payload/platform/plugins/email/references/email-reference.md +19 -15
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +3 -2
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +2 -1
  40. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +2 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.d.ts +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.d.ts.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.js +3 -2
  47. package/payload/platform/plugins/memory/mcp/dist/tools/session-retrospective-skip-rate.js.map +1 -1
  48. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +15 -0
  49. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +1 -1
  50. package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +1 -1
  51. package/payload/platform/scripts/check-cypher-int-params.mjs +277 -0
  52. package/payload/platform/scripts/setup-account.sh +2 -1
  53. package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
  54. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -2
  55. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  56. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +33 -1
  57. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  58. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +5 -2
  59. package/payload/server/{chunk-L2YK2VK3.js → chunk-EFJ2EXZK.js} +3 -1
  60. package/payload/server/maxy-edge.js +1 -1
  61. package/payload/server/server.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.185",
3
+ "version": "0.1.187",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for prompt-optimiser-directive.sh.
3
+ #
4
+ # Covers:
5
+ # 1. Valid UserPromptSubmit stdin -> exit 0, stdout is valid JSON,
6
+ # hookEventName == "UserPromptSubmit", additionalContext non-empty.
7
+ # 2. Empty / garbage stdin -> exit 0, still valid JSON (the hook
8
+ # ignores the prompt text; the directive is standing).
9
+ # 3. python3 unavailable on PATH -> exit 0, empty stdout (fail-open).
10
+
11
+ set -u
12
+
13
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/prompt-optimiser-directive.sh"
14
+ if [[ ! -x "$HOOK" ]]; then
15
+ echo "FAIL: $HOOK not executable" >&2
16
+ exit 1
17
+ fi
18
+
19
+ PASS=0
20
+ FAIL=0
21
+
22
+ # Assert: hook on $1 (stdin) exits 0 and stdout is JSON with a non-empty
23
+ # additionalContext and the correct hookEventName.
24
+ assert_valid_injection() {
25
+ local name="$1" stdin="$2" out actual_exit
26
+ out=$(printf '%s' "$stdin" | bash "$HOOK" 2>/dev/null)
27
+ actual_exit=$?
28
+ if [[ "$actual_exit" -ne 0 ]]; then
29
+ echo "FAIL: $name (expected exit=0, got=$actual_exit)" >&2; FAIL=$((FAIL+1)); return
30
+ fi
31
+ if printf '%s' "$out" | python3 -c '
32
+ import json, sys
33
+ d = json.load(sys.stdin)
34
+ h = d["hookSpecificOutput"]
35
+ assert h["hookEventName"] == "UserPromptSubmit", "wrong hookEventName"
36
+ assert isinstance(h["additionalContext"], str) and len(h["additionalContext"]) > 0, "empty additionalContext"
37
+ ' 2>/dev/null; then
38
+ echo "PASS: $name"; PASS=$((PASS+1))
39
+ else
40
+ echo "FAIL: $name (stdout not valid directive JSON: $out)" >&2; FAIL=$((FAIL+1))
41
+ fi
42
+ }
43
+
44
+ # Case 1 — valid UserPromptSubmit envelope.
45
+ assert_valid_injection "valid UserPromptSubmit stdin -> directive JSON" \
46
+ '{"hook_event_name":"UserPromptSubmit","prompt":"fix the thing","session_id":"abc"}'
47
+
48
+ # Case 2 — garbage stdin still yields the standing directive.
49
+ assert_valid_injection "garbage stdin -> still directive JSON" \
50
+ 'not json at all'
51
+
52
+ # Case 3 — fail-open when python3 is absent from PATH.
53
+ FAKEBIN=$(mktemp -d)
54
+ # A minimal PATH containing the core utils the hook needs (cat, printf, env,
55
+ # command) but NOT python3. Resolve their real dir(s) and symlink them in.
56
+ for util in cat printf env bash; do
57
+ src=$(command -v "$util" 2>/dev/null) && ln -sf "$src" "$FAKEBIN/$util" 2>/dev/null
58
+ done
59
+ nopy_out=$(printf '%s' '{"prompt":"x"}' | PATH="$FAKEBIN" bash "$HOOK" 2>/dev/null)
60
+ nopy_exit=$?
61
+ if [[ "$nopy_exit" -eq 0 && -z "$nopy_out" ]]; then
62
+ echo "PASS: python3 absent -> exit 0, empty stdout (fail-open)"; PASS=$((PASS+1))
63
+ else
64
+ echo "FAIL: python3 absent (expected exit=0 + empty stdout, got exit=$nopy_exit stdout=$nopy_out)" >&2; FAIL=$((FAIL+1))
65
+ fi
66
+ rm -rf "$FAKEBIN"
67
+
68
+ echo "----"
69
+ echo "PASS=$PASS FAIL=$FAIL"
70
+ [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit hook: inject the standing prompt-optimiser directive as
3
+ # additionalContext on every turn, so the agent restates each non-trivial
4
+ # prompt via the prompt-optimiser skill before acting on it. Mirrors
5
+ # doctrine.sh: emit hookSpecificOutput.additionalContext via python3,
6
+ # fail-open with exit 0.
7
+ #
8
+ # Compliance is behavioural, not deterministic. A hook cannot force a skill
9
+ # call; the directive steers the agent, it does not gate the turn. The raw
10
+ # prompt always still reaches the model (UserPromptSubmit output is ADDED to
11
+ # the prompt, never substituted), so "the restatement governs" is an
12
+ # instruction to the agent, not a removal of the original text.
13
+ #
14
+ # Input (stdin, UserPromptSubmit contract): { "prompt": "...", ... } — drained
15
+ # and ignored; the directive is standing and prompt-independent.
16
+ # Output (stdout JSON):
17
+ # { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
18
+ # "additionalContext": "<directive>" } }
19
+ #
20
+ # Fail-open: if python3 is unavailable or JSON encoding fails, exit 0 with no
21
+ # stdout. The hook must never block or corrupt a turn.
22
+
23
+ set -uo pipefail
24
+
25
+ # Drain stdin; the directive does not depend on the prompt text.
26
+ cat >/dev/null 2>&1 || true
27
+
28
+ # Fail-open if the JSON encoder is unavailable.
29
+ command -v python3 >/dev/null 2>&1 || exit 0
30
+
31
+ DIRECTIVE=$(cat <<'DIRECTIVE_EOF'
32
+ PROMPT-OPTIMISER DIRECTIVE (standing)
33
+ Before acting on this turn's request, run the prompt-optimiser skill on the user's raw prompt in its in-session restatement mode, then treat the restated task as the authoritative statement of what was asked and proceed from it. Set aside your own first reading of the raw wording; the restatement governs. The raw prompt stays in context for reference only.
34
+ Skip the restatement when the prompt is a one-word confirmation, a slash-command invocation, or a direct continuation of the immediately preceding turn; restatement adds nothing in those cases.
35
+ DIRECTIVE_EOF
36
+ )
37
+
38
+ OUT=$(printf '%s' "$DIRECTIVE" | python3 -c '
39
+ import json, sys
40
+ ctx = sys.stdin.read()
41
+ print(json.dumps({
42
+ "hookSpecificOutput": {
43
+ "hookEventName": "UserPromptSubmit",
44
+ "additionalContext": ctx,
45
+ }
46
+ }))
47
+ ' 2>/dev/null) || exit 0
48
+
49
+ [ -n "$OUT" ] || exit 0
50
+ printf '%s\n' "$OUT"
51
+ echo "[prompt-optimiser-directive] injected len=${#DIRECTIVE}" >&2
52
+ exit 0
@@ -2,8 +2,8 @@
2
2
  name: a4-print-documents
3
3
  description: >
4
4
  Constraints for producing print-quality A4 HTML documents intended for PDF output
5
- via browser_pdf_save. Use when creating or modifying any HTML document that will be
6
- saved as PDF, uses A4 page dimensions, or contains glassmorphism/backdrop-filter effects
5
+ via the on-device browser-pdf-save tool. Use when creating or modifying any HTML document
6
+ that will be saved as PDF, uses A4 page dimensions, or contains glassmorphism/backdrop-filter effects
7
7
  that must survive print. Trigger phrases: "PDF", "print to PDF", "download PDF",
8
8
  "one-pager", "A4", "brochure", "executive document", "market analysis", or any request
9
9
  to generate a document for print or download.
@@ -11,11 +11,13 @@ description: >
11
11
 
12
12
  # A4 Print-Ready HTML Documents
13
13
 
14
- Constraints for producing HTML documents that render correctly when saved as PDF via the personal-assistant's `browser_pdf_save` tool. Every rule exists because violating it caused a visible problem in production documents.
14
+ Constraints for producing HTML documents that render correctly when saved as PDF via the on-device `browser-pdf-save` tool. Every rule exists because violating it caused a visible problem in production documents.
15
15
 
16
16
  ## Rendering path
17
17
 
18
- Generate the HTML document, then delegate to the personal-assistant to save it as PDF using `browser_pdf_save`. The personal-assistant controls Playwright you compose the delegation brief describing what to render and any pre-render steps (like glassmorphism screenshot capture).
18
+ Generate the HTML document, then save it as PDF with the on-device browser tools (no Playwright). The order is: capture any glassmorphism print-fallback images first (see below), then `browser-navigate` to the served HTML and `browser-pdf-save` to the output path. `browser-pdf-save` uses Chromium's print pipeline, so `@page` rules, page breaks, and `print-color-adjust` all apply and backgrounds are printed.
19
+
20
+ Serve the HTML over `http://127.0.0.1:<port>` and navigate to that URL — open it from a local server rather than relying on a `file://` path. `browser-navigate` keeps the page alive so a screenshot pass and the PDF pass act on the same rendered document.
19
21
 
20
22
  ## Glassmorphism does not survive print
21
23
 
@@ -50,19 +52,17 @@ This pattern applies to every full-bleed glassmorphism section (cover pages, bac
50
52
 
51
53
  ### Capturing the print image
52
54
 
53
- Each glassmorphism section needs its own screenshot — you cannot reuse a print image from a different document or a differently-sized section.
54
-
55
- Delegate to the personal-assistant with a brief that includes:
55
+ Each glassmorphism section needs its own screenshot — you cannot reuse a print image from a different document or a differently-sized section. Capture it with the on-device browser tools against the served HTML:
56
56
 
57
- 1. **Serve the HTML over HTTP** `file://` protocol is blocked by Playwright. The specialist should start a local server.
58
- 2. **Set the viewport** to match the element's rendered size. Measure the element's bounding box first, then resize the viewport to `Math.ceil(width) + 1` by `Math.ceil(height) + 1` to eliminate scrollbars.
59
- 3. **Hide UI chrome** (download buttons, navigation overlays) and set `document.body.style.overflow = 'hidden'` to suppress body-level scrollbars.
60
- 4. **Screenshot the specific element** (e.g. `.cover`, `.backpage`) using element-level screenshot not a full-page or viewport screenshot.
57
+ 1. **Serve the HTML over HTTP** and `browser-navigate` to `http://127.0.0.1:<port>/...` open it from a local server, not a `file://` path.
58
+ 2. **Match the viewport to the element.** Read the element's bounding box with `browser-evaluate` (`document.querySelector('.cover').getBoundingClientRect()`), then `browser-resize` to `Math.ceil(width) + 1` by `Math.ceil(height) + 1` to eliminate scrollbars.
59
+ 3. **Hide UI chrome** with `browser-evaluate` — hide download buttons and navigation overlays and set `document.body.style.overflow = 'hidden'` to suppress body-level scrollbars.
60
+ 4. **Capture the specific element** with `browser-screenshot` passing the element's CSS selector (e.g. `.cover`, `.backpage`) the selector clips to that element, not the full page.
61
61
  5. **Save the PNG** alongside the HTML, named for the section it replaces (e.g. `cover-print.png`, `backpage-print.png`).
62
- 6. **Restore** hidden elements and overflow after capture.
62
+ 6. **Restore** hidden elements and overflow with `browser-evaluate` after capture.
63
63
  7. **Verify** the saved PNG visually — confirm no scrollbars, no UI chrome, correct dimensions.
64
64
 
65
- After capturing all print images, delegate a second task to save the final PDF via `browser_pdf_save`.
65
+ After capturing all print images, save the final PDF with `browser-pdf-save` to the output path.
66
66
 
67
67
  ## Page margins, page numbers, and running footers
68
68
 
@@ -211,7 +211,7 @@ Multi-page documents follow this structure:
211
211
  </div>
212
212
  ```
213
213
 
214
- The backpage print image is captured via the same personal-assistant delegation process as the cover — element-level screenshot with UI chrome hidden.
214
+ The backpage print image is captured the same way as the cover — `browser-screenshot` with the element selector, UI chrome hidden via `browser-evaluate`.
215
215
 
216
216
  ## Single-page documents
217
217
 
@@ -17,7 +17,7 @@ Constraints for producing HTML slide decks that render correctly on screen and e
17
17
 
18
18
  ## How this differs from A4 print documents
19
19
 
20
- A4 documents use the browser's native print pipeline (`@page` rules, Playwright `browser_pdf_save`). Deck pages bypass print entirely — each slide is rasterized to a canvas and composited into a PDF on the client. This means:
20
+ A4 documents use the browser's native print pipeline (`@page` rules, the on-device `browser-pdf-save` tool). Deck pages bypass print entirely — each slide is rasterized to a canvas and composited into a PDF on the client. This means:
21
21
 
22
22
  - Glassmorphism and `backdrop-filter` render correctly (no PNG fallbacks needed).
23
23
  - Text in the PDF is raster, not vector — so the canvas `scale` factor matters for sharpness.
@@ -29,7 +29,7 @@ Print styles are still included as a fallback for users who Cmd+P, but they are
29
29
 
30
30
  1. Build the HTML deck with fixed-dimension `.slide` containers.
31
31
  2. Add a client-side download button that uses `html2canvas-pro` + `jsPDF`.
32
- 3. Verify each slide visually using Playwright snapshots before declaring the deck complete.
32
+ 3. Verify each slide visually using `browser-screenshot` before declaring the deck complete.
33
33
 
34
34
  ## Slide dimensions
35
35
 
@@ -316,25 +316,17 @@ Slides use a compressed typography scale that fits content within the fixed 210m
316
316
 
317
317
  These sizes are calibrated for 297mm × 210mm at `scale: 2` — they produce readable text in the exported PDF. Smaller sizes become illegible; larger sizes cause overflow.
318
318
 
319
- ## Verifying slides with Playwright snapshots
319
+ ## Verifying slides with on-device screenshots
320
320
 
321
- Before declaring a deck complete, verify that every slide renders correctly by taking Playwright screenshots. This catches problems that are invisible in the dev server but appear in the PDF: clipped content, broken images, font loading failures, and layout overflow.
321
+ Before declaring a deck complete, verify that every slide renders correctly by taking screenshots with `browser-screenshot`. This catches problems that are invisible in the dev server but appear in the PDF: clipped content, broken images, font loading failures, and layout overflow.
322
322
 
323
323
  ### Verification process
324
324
 
325
325
  1. **Start a local server** — the deck must be served over HTTP. `file://` protocol blocks font loading and CORS resources. If Next.js, run `npm run dev` and wait for the ready signal before navigating.
326
326
 
327
- 2. **Navigate to the deck page** and wait for fonts and images to load. Use `browser_wait_for` or `browser_evaluate` with `document.fonts.ready` to ensure custom fonts are loaded — otherwise screenshots capture system-font fallbacks that look correct in dev but produce the wrong PDF.
327
+ 2. **Navigate to the deck page** with `browser-navigate` and wait for fonts and images to load. Use `browser-wait-for` or `browser-evaluate` with `document.fonts.ready` to ensure custom fonts are loaded — otherwise screenshots capture system-font fallbacks that look correct in dev but produce the wrong PDF.
328
328
 
329
- 3. **Screenshot each slide individually** — take an element-level screenshot of each `.slide` div, not a full-page screenshot. Full-page screenshots compress the entire deck into one image and hide per-slide problems.
330
-
331
- Pseudocode for the Playwright loop:
332
- ```
333
- slides = page.locator(".slide").all()
334
- for i, slide in enumerate(slides):
335
- slide.scroll_into_view_if_needed()
336
- slide.screenshot(path=f"slide-{i+1}.png")
337
- ```
329
+ 3. **Screenshot each slide individually** — call `browser-screenshot` with each `.slide` element's CSS selector, not a full-page screenshot. Full-page screenshots compress the entire deck into one image and hide per-slide problems. Read the slide selectors from `browser-snapshot` (or compute `.slide:nth-of-type(n)`), and capture each to its own `slide-<n>.png`.
338
330
 
339
331
  4. **Verify each screenshot against these checks:**
340
332
  - Content fits within the slide bounds (no clipping at edges).
@@ -345,15 +337,9 @@ Before declaring a deck complete, verify that every slide renders correctly by t
345
337
  - No unexpected scrollbars or overflow indicators.
346
338
  - Card grids and tables are evenly spaced.
347
339
 
348
- 5. **Test the PDF download** — click the download button via Playwright and verify the generated PDF:
349
- ```
350
- page.click("button:has-text('Download PDF')")
351
- download = page.wait_for_event("download")
352
- download.save_as("deck.pdf")
353
- ```
354
- Then open the PDF and confirm page count matches slide count and no pages are blank.
340
+ 5. **Test the PDF download** — `browser-click` the download button, then open the generated PDF and confirm the page count matches the slide count and no pages are blank.
355
341
 
356
- ### Common rendering failures caught by snapshots
342
+ ### Common rendering failures caught by screenshots
357
343
 
358
344
  | Symptom | Cause | Fix |
359
345
  |---------|-------|-----|
@@ -413,6 +399,6 @@ A complete working deck lives at `/Users/neo/capacity-derivatives/app/deck/` —
413
399
  - [ ] Download button is outside `.slide` containers (in a fixed overlay).
414
400
  - [ ] Dark slides use `backgroundColor: null` in html2canvas config.
415
401
  - [ ] Print fallback styles included (`@page`, `@media print`).
416
- - [ ] Playwright screenshots taken of every slide — content verified.
402
+ - [ ] `browser-screenshot` taken of every slide — content verified.
417
403
  - [ ] PDF download tested — correct number of pages, no blank pages.
418
404
  - [ ] Fonts loaded before any screenshot or export (check `document.fonts.ready`).
@@ -141,7 +141,7 @@ Before saving the PDF, ask the operator where they want it saved. The operator n
141
141
 
142
142
  ### 9. Save the PDF
143
143
 
144
- Use `browser_pdf_save` to write the HTML to the operator-named path.
144
+ `browser-navigate` to the served HTML, then `browser-pdf-save` to write the PDF to the operator-named path (on-device Chromium print pipeline; no Playwright).
145
145
 
146
146
  Emit `[professional-document] pdf-saved path=<operator-named-path> bytes=<n>`.
147
147
 
@@ -46,7 +46,7 @@ Ask what rules the agent should follow when using this skill:
46
46
 
47
47
  Not every skill needs all of these. Only include what's relevant.
48
48
 
49
- If the skill involves generating PDFs or print-ready documents, load `references/pdf-generation.md` for the constraints on HTML layout and the `browser_pdf_save` rendering path.
49
+ If the skill involves generating PDFs or print-ready documents, load `references/pdf-generation.md` for the constraints on HTML layout and the `browser-pdf-save` rendering path.
50
50
 
51
51
  ## Step 4: Decide on References
52
52
 
@@ -1,15 +1,15 @@
1
1
  # PDF Generation in Skills
2
2
 
3
- When a skill needs to produce a PDF, the output is always a two-stage process: the skill authors the HTML, and the personal-assistant renders it to PDF via the `browser_pdf_save` MCP tool.
3
+ When a skill needs to produce a PDF, the output is always a two-stage process: the skill authors the HTML, and the agent renders it to PDF with the on-device `browser-pdf-save` tool.
4
4
 
5
5
  ## The rule
6
6
 
7
- Never instruct the agent to write or execute raw Node.js scripts (e.g. `require('playwright')`, `page.pdf()`, `npx playwright`). Playwright is not in the Node.js module path — these scripts fail with MODULE_NOT_FOUND, waste tool calls searching the filesystem, and produce hardcoded paths that break on reinstall.
7
+ Never instruct the agent to write or execute raw Node.js scripts (e.g. `require('playwright')`, `page.pdf()`, `npx playwright`). Playwright is not on device — these scripts fail with MODULE_NOT_FOUND, waste tool calls searching the filesystem, and produce hardcoded paths that break on reinstall.
8
8
 
9
9
  Instead, skills that produce PDFs should:
10
10
 
11
11
  1. **Build the HTML** — the skill's behaviour section describes what the document contains and how it's structured. The agent writes the HTML file using standard file tools.
12
- 2. **Delegate rendering to personal-assistant** — the agent asks the personal-assistant to navigate to the HTML file and call `browser_pdf_save` to produce the PDF. The specialist has Playwright access through governed MCP tools; the admin agent does not.
12
+ 2. **Render with `browser-pdf-save`**serve the HTML over `http://127.0.0.1:<port>`, `browser-navigate` to it, then `browser-pdf-save` to the output path. The tool drives the device's Chromium over CDP (honours `@page`/print CSS, prints backgrounds); no Playwright is involved.
13
13
 
14
14
  ## Print-ready HTML constraints
15
15
 
@@ -27,4 +27,4 @@ HTML intended for PDF output must follow print-ready layout patterns. The most c
27
27
 
28
28
  ## What the skill file should contain
29
29
 
30
- The skill's SKILL.md describes the document's purpose and structure. A `references/` file holds the HTML template or content specification. The skill should state that PDF rendering is delegated to personal-assistant — it should not contain Playwright code, npm commands, or file path assumptions about the Playwright installation.
30
+ The skill's SKILL.md describes the document's purpose and structure. A `references/` file holds the HTML template or content specification. The skill should state that PDF rendering goes through `browser-pdf-save` — it should not contain Playwright code, npm commands, or file path assumptions about any Playwright installation.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser",
3
- "description": "Headless browser rendering for JavaScript-heavy pages. Provides browser-render, which drives the device's existing Chromium over the Chrome DevTools Protocol and returns a page's fully rendered DOM (HTML + visible text). This is the JS-rendering leg of the three-tool retrieval story: WebFetch (lossy summary) / url-get (verbatim, server-rendered) / browser-render (JS-rendered). Use it for client-rendered SPAs and pages that WebFetch or url-get return empty or as a shell.",
3
+ "description": "Drives the device's existing Chromium over the Chrome DevTools Protocol for both retrieval and interactive automation. browser-render reads a JS-rendered page's DOM (the JS-rendering leg of WebFetch / url-get / browser-render retrieval). The browser-navigate family keeps one page alive across calls for click/fill/snapshot automation, and browser-pdf-save / browser-screenshot turn the rendered page into PDF or PNG files on device. Use it for client-rendered SPAs, form automation, and generating A4 PDFs.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,10 +1,61 @@
1
1
  ---
2
2
  name: browser
3
- description: "Headless browser rendering for JavaScript-heavy pages. Provides browser-render, which drives the device's existing Chromium over the Chrome DevTools Protocol and returns a page's fully rendered DOM (HTML + visible text). This is the JS-rendering leg of the three-tool retrieval story: WebFetch (lossy summary) / url-get (verbatim, server-rendered) / browser-render (JS-rendered). Use it for client-rendered SPAs and pages that WebFetch or url-get return empty or as a shell."
3
+ description: "Drives the device's existing Chromium over the Chrome DevTools Protocol for both retrieval and interactive automation. browser-render reads a JS-rendered page's DOM (the JS-rendering leg of WebFetch / url-get / browser-render retrieval). The browser-navigate family keeps one page alive across calls for click/fill/snapshot automation, and browser-pdf-save / browser-screenshot turn the rendered page into PDF or PNG files on device. Use it for client-rendered SPAs, form automation, and generating A4 PDFs."
4
4
  tools:
5
5
  - name: browser-render
6
6
  publicAllowlist: false
7
7
  adminAllowlist: true
8
+ - name: browser-navigate
9
+ publicAllowlist: false
10
+ adminAllowlist: true
11
+ - name: browser-snapshot
12
+ publicAllowlist: false
13
+ adminAllowlist: true
14
+ - name: browser-click
15
+ publicAllowlist: false
16
+ adminAllowlist: true
17
+ - name: browser-fill
18
+ publicAllowlist: false
19
+ adminAllowlist: true
20
+ - name: browser-fill-form
21
+ publicAllowlist: false
22
+ adminAllowlist: true
23
+ - name: browser-type
24
+ publicAllowlist: false
25
+ adminAllowlist: true
26
+ - name: browser-press-key
27
+ publicAllowlist: false
28
+ adminAllowlist: true
29
+ - name: browser-hover
30
+ publicAllowlist: false
31
+ adminAllowlist: true
32
+ - name: browser-select-option
33
+ publicAllowlist: false
34
+ adminAllowlist: true
35
+ - name: browser-wait-for
36
+ publicAllowlist: false
37
+ adminAllowlist: true
38
+ - name: browser-handle-dialog
39
+ publicAllowlist: false
40
+ adminAllowlist: true
41
+ - name: browser-evaluate
42
+ publicAllowlist: false
43
+ adminAllowlist: true
44
+ - name: browser-console-messages
45
+ publicAllowlist: false
46
+ adminAllowlist: true
47
+ - name: browser-tabs
48
+ publicAllowlist: false
49
+ adminAllowlist: true
50
+ - name: browser-pdf-save
51
+ publicAllowlist: false
52
+ adminAllowlist: true
53
+ - name: browser-screenshot
54
+ publicAllowlist: false
55
+ adminAllowlist: true
56
+ - name: browser-resize
57
+ publicAllowlist: false
58
+ adminAllowlist: true
8
59
  always: true
9
60
  embed: false
10
61
  metadata: {"platform":{}}
@@ -23,22 +74,41 @@ mcp-manifest: auto
23
74
 
24
75
  # Browser
25
76
 
26
- Renders JavaScript-heavy web pages and returns the rendered DOM, so the agent can read content that only exists after the page's scripts run.
77
+ Drives the device's Chromium over the Chrome DevTools Protocol for three things: reading JS-rendered pages, automating a page (click/fill/snapshot), and turning a page into a PDF or PNG file on device.
27
78
 
28
79
  ## Why it exists
29
80
 
30
- `WebFetch` summarises and refuses verbatim output; `url-get` returns the verbatim source but does not execute JavaScript. Client-rendered pages (SPAs, content injected by in-page scripts) come back from both as an empty shell. `browser-render` is the only retrieval path that runs the page's JavaScript and reads the resulting DOM.
81
+ `WebFetch` summarises and refuses verbatim output; `url-get` returns the verbatim source but does not execute JavaScript. Client-rendered pages come back from both as an empty shell. `browser-render` runs the page's JavaScript and reads the resulting DOM. The automation and output tools exist because the specialists previously named a Playwright tool surface (interaction + `browser_pdf_save`) that never actually shipped on a Pi — these tools provide the same capabilities backed by the on-device CDP connection instead.
31
82
 
32
83
  ## How it works
33
84
 
34
- The device already runs a per-brand Chromium for the VNC surface, launched by `platform/scripts/vnc.sh` with `--remote-debugging-port=${CDP_PORT}`. `browser-render` attaches to that running browser over the Chrome DevTools Protocol — it never launches its own browser, so nothing extra ships on device and there is no on-demand download.
85
+ The device already runs a per-brand Chromium for the VNC surface, launched by `platform/scripts/vnc.sh` with `--remote-debugging-port=${CDP_PORT}`. Every tool attaches to that running browser over the Chrome DevTools Protocol — it never launches its own browser, so nothing extra ships on device and there is no on-demand download. The brand's CDP port reaches this server through the `CDP_PORT` spawn placeholder.
86
+
87
+ `browser-render` is **stateless**: each call opens a fresh background target, navigates, reads the DOM, and closes it. The automation tools share **one persistent page**: `browser-navigate` creates (or reuses) a single live target, and the action tools drive it across calls. If that page dies (operator closes the tab, crash, browser restart) the action tools return `session-lost` and the agent calls `browser-navigate` again — only navigate re-attaches, so a dead page is never silently replaced mid-action. `browser-tabs` opens/switches/closes pages.
88
+
89
+ ## Tools
90
+
91
+ Retrieval (stateless):
92
+ - `browser-render` — `url`, optional `loadTimeoutMs`. Returns rendered HTML + visible text.
35
93
 
36
- For each call it opens a fresh background target, navigates to the URL, waits for the page load event, reads `document.documentElement.outerHTML` and the visible `body.innerText`, then closes the target. The brand's CDP port reaches this server through the `CDP_PORT` spawn placeholder.
94
+ Automation (shared persistent page; selectors come from `browser-snapshot`):
95
+ - `browser-navigate` — open a URL and keep the page alive. Entry point for automation.
96
+ - `browser-snapshot` — accessibility snapshot: each interactive/landmark node as role, name, and a unique CSS selector to use with click/fill/hover.
97
+ - `browser-click`, `browser-fill`, `browser-fill-form`, `browser-hover`, `browser-select-option` — act on a CSS selector.
98
+ - `browser-type`, `browser-press-key` — real keyboard input (`Input.*`).
99
+ - `browser-wait-for` — wait for a selector or text to appear.
100
+ - `browser-handle-dialog` — pre-arm how the next JS dialog(s) are answered (CDP dialogs block, so policy must be set before the triggering action).
101
+ - `browser-evaluate` — run a JS expression in the page and return its value.
102
+ - `browser-console-messages` — drain buffered console output + uncaught errors.
103
+ - `browser-tabs` — list / new / select / close pages.
37
104
 
38
- ## Tool
105
+ Output (to a file on device):
106
+ - `browser-pdf-save` — `Page.printToPDF` of the current page; the on-device replacement for the old Playwright `browser_pdf_save`.
107
+ - `browser-screenshot` — PNG of the page or one element (CSS selector clip), e.g. glassmorphism print-fallback images.
108
+ - `browser-resize` — set the viewport size.
39
109
 
40
- - `browser-render` input: `url` (absolute http(s)), optional `loadTimeoutMs`. Returns the rendered HTML and visible text, or a structured failure (`cdp-unreachable`, `navigate-failed`, `load-timeout`, `evaluate-failed`, `websocket-unavailable`). Admin-allowlisted; not exposed to public chat.
110
+ All tools are admin-allowlisted; none are exposed to public chat.
41
111
 
42
112
  ## Observability
43
113
 
44
- A successful render emits `[browser-render] url=… rendered=true domBytes=…` on the server's stderr (captured by the mcp-spawn-tee log). Failures emit `rendered=false outcome=… detail=…`. On a device with no running Chromium (e.g. a laptop native-display install with no VNC browser) the tool returns `cdp-unreachable` rather than hanging.
114
+ A successful render emits `[browser-render] url=… rendered=true domBytes=…` on the server's stderr (captured by the mcp-spawn-tee log). Every other tool returns a structured outcome: `ok`, or one of `cdp-unreachable`, `session-lost`, `selector-not-found`, `option-not-found`, `wait-timeout`, `navigate-failed`, `load-timeout`, `evaluate-failed`, `command-timeout`, `pdf-failed`, `screenshot-failed`, `unsupported-key`. On a device with no running Chromium (e.g. a laptop native-display install with no VNC browser) the tools return `cdp-unreachable` rather than hanging.
@@ -4,7 +4,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
6
6
  import { z } from "zod";
7
+ import { writeFile } from "node:fs/promises";
7
8
  import { renderPage } from "./lib/cdp-render.js";
9
+ import { BrowserSession, DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_LOAD_TIMEOUT_MS, } from "./lib/cdp-session.js";
10
+ import * as actions from "./lib/cdp-actions.js";
8
11
  // Cast to `any` to avoid TS2589 the same way the other plugin servers do.
9
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
13
  const server = new McpServer({ name: "maxy-browser", version: "0.1.0" });
@@ -14,6 +17,9 @@ const cdpPortRaw = process.env.CDP_PORT ?? "";
14
17
  const cdpPort = Number.parseInt(cdpPortRaw, 10);
15
18
  process.stderr.write(`[browser] boot cdpPort=${cdpPortRaw || "-"}\n`);
16
19
  const log = (line) => process.stderr.write(`${line}\n`);
20
+ const WebSocketImpl = typeof WebSocket !== "undefined"
21
+ ? WebSocket
22
+ : undefined;
17
23
  // Cap what we hand back to the model so a large SPA cannot blow the agent's
18
24
  // context window. domBytes in the header always reports the true size.
19
25
  const MAX_TEXT_CHARS = 200_000;
@@ -27,13 +33,7 @@ eagerTool(server, "browser-render", "Render a JavaScript-heavy web page in the d
27
33
  url: z.string().url().describe("Absolute http(s) URL to render"),
28
34
  loadTimeoutMs: z.number().int().positive().optional().describe("Max wait for the page load event (default 30000)"),
29
35
  }, async (params) => {
30
- const result = await renderPage(params.url, { loadTimeoutMs: params.loadTimeoutMs }, {
31
- fetchImpl: fetch,
32
- WebSocketImpl: typeof WebSocket !== "undefined" ? WebSocket : undefined,
33
- cdpHost: "127.0.0.1",
34
- cdpPort,
35
- log,
36
- });
36
+ const result = await renderPage(params.url, { loadTimeoutMs: params.loadTimeoutMs }, { fetchImpl: fetch, WebSocketImpl, cdpHost: "127.0.0.1", cdpPort, log });
37
37
  if (!result.rendered) {
38
38
  return {
39
39
  content: [{
@@ -55,6 +55,110 @@ eagerTool(server, "browser-render", "Render a JavaScript-heavy web page in the d
55
55
  }],
56
56
  };
57
57
  });
58
+ // ── Interactive automation + output tools ────────────────────────────────────
59
+ //
60
+ // All of these drive ONE persistent page held in `browserSession`, against the
61
+ // same device Chromium over CDP. The page is created by browser-navigate and
62
+ // reused across calls; browser-tabs opens/switches/closes pages. A dead page
63
+ // (operator closed the tab, crash, browser restart) yields a `session-lost`
64
+ // outcome telling the agent to navigate again — only navigate re-attaches.
65
+ const cdpDeps = { fetchImpl: fetch, WebSocketImpl, cdpHost: "127.0.0.1", cdpPort, log };
66
+ const browserSession = new BrowserSession(cdpDeps);
67
+ const actionCtx = {
68
+ deps: cdpDeps,
69
+ commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
70
+ loadTimeoutMs: DEFAULT_LOAD_TIMEOUT_MS,
71
+ writeFile: (path, data) => writeFile(path, data),
72
+ };
73
+ function reply(label, r, successText) {
74
+ if (!r.ok) {
75
+ return {
76
+ content: [{ type: "text", text: `${label} failed: ${r.outcome}${r.detail ? ` — ${r.detail}` : ""}` }],
77
+ isError: true,
78
+ };
79
+ }
80
+ return { content: [{ type: "text", text: successText ?? `${label}: ok` }] };
81
+ }
82
+ eagerTool(server, "browser-navigate", "Open a URL in the device's Chromium and keep the page alive for follow-up automation (click, fill, snapshot, pdf-save, screenshot). This is the entry point for interactive browser work — call it before any other browser-* automation tool. Reuses the one live automation page; use browser-tabs to open a separate page. For one-shot read-only retrieval of a JS page use browser-render instead.", {
83
+ url: z.string().url().describe("Absolute http(s) URL to open"),
84
+ loadTimeoutMs: z.number().int().positive().optional().describe("Max wait for the page load event (default 30000)"),
85
+ }, async (params) => reply("browser-navigate", await actions.navigate(browserSession, params, actionCtx), `Navigated to ${params.url}`));
86
+ eagerTool(server, "browser-snapshot", "Return an accessibility snapshot of the current page: each interactive or landmark element as role, accessible name, and a unique CSS selector. Use the returned selector verbatim with browser-click / browser-fill / browser-hover — it targets exactly the element you saw. Call this after browser-navigate (or after an action changes the page) to know what is on the page.", {}, async () => {
87
+ const r = await actions.snapshot(browserSession, {}, actionCtx);
88
+ return reply("browser-snapshot", r, r.data?.text);
89
+ });
90
+ eagerTool(server, "browser-click", "Click the element matching a CSS selector on the current page. Use a selector from browser-snapshot. Returns selector-not-found if nothing matches.", { selector: z.string().describe("CSS selector of the element to click") }, async (params) => reply("browser-click", await actions.click(browserSession, params, actionCtx), `Clicked ${params.selector}`));
91
+ eagerTool(server, "browser-fill", "Set the value of one input, textarea, or contenteditable matching a CSS selector, firing input/change events. For multiple fields in one call use browser-fill-form.", {
92
+ selector: z.string().describe("CSS selector of the field"),
93
+ value: z.string().describe("Value to set"),
94
+ }, async (params) => reply("browser-fill", await actions.fill(browserSession, params, actionCtx), `Filled ${params.selector}`));
95
+ eagerTool(server, "browser-fill-form", "Fill several fields in one call. Each field is a CSS selector and the value to set. Reports every selector that matched nothing.", {
96
+ fields: z.array(z.object({
97
+ selector: z.string().describe("CSS selector of the field"),
98
+ value: z.string().describe("Value to set"),
99
+ })).describe("Fields to fill"),
100
+ }, async (params) => reply("browser-fill-form", await actions.fillForm(browserSession, params, actionCtx), `Filled ${params.fields.length} field(s)`));
101
+ eagerTool(server, "browser-type", "Type text into the focused element via real keyboard input. Pass a selector to focus that element first. Use this when setting value directly (browser-fill) does not trigger the page's keystroke handlers (autocompletes, rich editors).", {
102
+ text: z.string().describe("Text to type"),
103
+ selector: z.string().optional().describe("Optional CSS selector to focus before typing"),
104
+ }, async (params) => reply("browser-type", await actions.type(browserSession, params, actionCtx), "Typed text"));
105
+ eagerTool(server, "browser-press-key", "Press a single key on the focused element (Enter, Tab, Escape, Backspace, Delete, Arrow keys, Home, End, PageUp, PageDown, Space, or a single character). Use for submitting forms (Enter) or keyboard navigation.", { key: z.string().describe("Key name or single character") }, async (params) => reply("browser-press-key", await actions.pressKey(browserSession, params, actionCtx), `Pressed ${params.key}`));
106
+ eagerTool(server, "browser-hover", "Hover the pointer over the element matching a CSS selector, firing pointer/mouse-over events. Use to reveal hover menus or tooltips before reading or clicking.", { selector: z.string().describe("CSS selector of the element to hover") }, async (params) => reply("browser-hover", await actions.hover(browserSession, params, actionCtx), `Hovered ${params.selector}`));
107
+ eagerTool(server, "browser-select-option", "Choose an option in a <select> dropdown by option value or visible label, firing a change event.", {
108
+ selector: z.string().describe("CSS selector of the <select>"),
109
+ value: z.string().optional().describe("Option value to select"),
110
+ label: z.string().optional().describe("Option visible text/label to select"),
111
+ }, async (params) => reply("browser-select-option", await actions.selectOption(browserSession, params, actionCtx), `Selected option in ${params.selector}`));
112
+ eagerTool(server, "browser-wait-for", "Wait until an element (CSS selector) appears or some text is present on the page, up to a timeout. Use after an action that loads content asynchronously, before reading or interacting.", {
113
+ selector: z.string().optional().describe("CSS selector to wait for"),
114
+ text: z.string().optional().describe("Visible text to wait for"),
115
+ timeoutMs: z.number().int().positive().optional().describe("Max wait (default 30000)"),
116
+ }, async (params) => reply("browser-wait-for", await actions.waitFor(browserSession, params, actionCtx), "Condition met"));
117
+ eagerTool(server, "browser-handle-dialog", "Arm how the next JavaScript dialog(s) — alert, confirm, prompt — on the current page are answered. Set this BEFORE the action that triggers the dialog, because dialogs block the page and cannot be answered after the fact. The policy stands until changed. Unarmed dialogs are auto-dismissed and reported by browser-console-messages.", {
118
+ accept: z.boolean().describe("true to accept/OK, false to dismiss/Cancel"),
119
+ promptText: z.string().optional().describe("Text to enter into a prompt() dialog when accepting"),
120
+ }, async (params) => reply("browser-handle-dialog", actions.handleDialog(browserSession, params), `Dialog policy armed: accept=${params.accept}`));
121
+ eagerTool(server, "browser-evaluate", "Run a JavaScript expression in the current page and return its value (awaits a returned promise). Use for reading page state or driving behaviour that no other tool covers. The expression runs in the page, not in Node.", { expression: z.string().describe("JavaScript expression to evaluate in the page") }, async (params) => {
122
+ const r = await actions.evaluate(browserSession, params, actionCtx);
123
+ return reply("browser-evaluate", r, `Result: ${JSON.stringify(r.data?.value ?? null)}`);
124
+ });
125
+ eagerTool(server, "browser-console-messages", "Return and clear the buffered console output and uncaught errors from the current page (captured since navigation). Use to debug why a page or interaction did not behave as expected.", {}, async () => {
126
+ const r = actions.consoleMessages(browserSession);
127
+ return reply("browser-console-messages", r, r.data?.text);
128
+ });
129
+ eagerTool(server, "browser-tabs", "Manage browser tabs/pages: list open pages, open a new page (optionally at a URL and switch to it), select an existing page by id, or close a page by id. Use 'new' or 'select' to get a fresh page so stale state from a prior task does not leak in.", {
130
+ action: z.enum(["list", "new", "select", "close"]).describe("Tab action"),
131
+ id: z.string().optional().describe("Target id (required for select/close)"),
132
+ url: z.string().optional().describe("URL to open (optional for new)"),
133
+ }, async (params) => {
134
+ const r = await actions.tabs(browserSession, params, actionCtx);
135
+ const text = params.action === "list"
136
+ ? "Tabs:\n" + (r.data?.tabs ?? [])
137
+ .map((t) => `${t.active ? "* " : " "}${t.id} — ${t.title || "(untitled)"} — ${t.url}`).join("\n")
138
+ : `tabs ${params.action}: ok`;
139
+ return reply("browser-tabs", r, text);
140
+ });
141
+ eagerTool(server, "browser-pdf-save", "Render the current page to a PDF file on the device using Chromium's print pipeline (honours @page/@media print CSS, prints backgrounds). Pass an absolute output path. This is the on-device replacement for the old Playwright browser_pdf_save: navigate to the HTML (served over http, or a file path the browser can open), then call this.", {
142
+ path: z.string().describe("Absolute output path for the .pdf file"),
143
+ landscape: z.boolean().optional().describe("Landscape orientation (default false)"),
144
+ scale: z.number().optional().describe("Scale of the page rendering (default 1)"),
145
+ printBackground: z.boolean().optional().describe("Print background graphics (default true)"),
146
+ }, async (params) => {
147
+ const r = await actions.pdfSave(browserSession, params, actionCtx);
148
+ return reply("browser-pdf-save", r, `Saved PDF to ${params.path} (${r.data?.bytes ?? 0} bytes)`);
149
+ });
150
+ eagerTool(server, "browser-screenshot", "Capture a PNG of the current page (or one element, via a CSS selector clip) to a file on the device. Pass an absolute output path. Use selector clips to capture glassmorphism/blur sections as print fallback images for A4 PDFs.", {
151
+ path: z.string().describe("Absolute output path for the .png file"),
152
+ selector: z.string().optional().describe("CSS selector to clip to a single element"),
153
+ fullPage: z.boolean().optional().describe("Capture beyond the viewport (default false)"),
154
+ }, async (params) => {
155
+ const r = await actions.screenshot(browserSession, params, actionCtx);
156
+ return reply("browser-screenshot", r, `Saved screenshot to ${params.path} (${r.data?.bytes ?? 0} bytes)`);
157
+ });
158
+ eagerTool(server, "browser-resize", "Set the current page's viewport size in CSS pixels. Use before a screenshot to control the rendered dimensions (e.g. match an element's measured size to avoid scrollbars).", {
159
+ width: z.number().int().positive().describe("Viewport width in CSS pixels"),
160
+ height: z.number().int().positive().describe("Viewport height in CSS pixels"),
161
+ }, async (params) => reply("browser-resize", await actions.resize(browserSession, params, actionCtx), `Resized to ${params.width}x${params.height}`));
58
162
  process.on("SIGINT", () => process.exit(0));
59
163
  const transport = new StdioServerTransport();
60
164
  await server.connect(transport);