frontpage-host 0.3.19 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -321,3 +321,7 @@ remain errors; they do not become original-file fallback successes.
321
321
  ## Native Booking
322
322
 
323
323
  Read [BOOKING.md](./BOOKING.md) before using `frontpage modules booking`. Use native settings, types/forms, members/spaces, availability, reservations and recurring blocks. Do not send new bookings through the old generic module collector. Every write requires a stable request key; updates require the current revision. Consent and private calendar-link entry stay with the human in Workspace → Connections. Ordinary owned connection management and self-hours share the workspace actions; setup requests appear in the editor status bar and overview.
324
+
325
+ ## Page screenshots
326
+
327
+ Use `frontpage screenshots capture --path /pricing --device mobile --wait --json` to inspect a built staging page, or `frontpage screenshots get CAPTURE_ID --json` to read a result. The site context comes from `--site` or `frontpage use`. Check the returned revision, coverage and warnings. Private-site images require authenticated reads; captures expire after 30 days. Use the tool when it helps, not on every turn or edit. See [SCREENSHOTS.md](SCREENSHOTS.md) for capture modes, retries, limits and image downloads.
package/BOOKING.md CHANGED
@@ -54,7 +54,7 @@ A recurring block update/removal adds `--scope this|following|all` and the exact
54
54
 
55
55
  Calendar consent stays with the person connecting the account. Never put OAuth credentials or private iCal URLs into CLI files, prompts or agent tools. Follow the returned workspace link and connect there. Each member controls their own calendars; admins can connect shared calendars to spaces. Hard refresh permits two distinct attempts per calendar in a rolling 15 minutes, including failures. The same retry identity does not spend another attempt. The dispatcher runs every five minutes; healthy reconciliation targets 15 minutes, plus scheduling delay and provider backoff.
56
56
 
57
- Google and Outlook import busy periods and can receive Frontpage-owned confirmed events when the owner grants write access. Private event details never reach visitors. iCal imports unavailable dates for approval-based stays; it does not send bookings back or provide a real-time double-booking guarantee.
57
+ Google and Outlook import busy periods and can receive Frontpage-owned confirmed events when the owner grants write access. Private event details never reach visitors. iCal imports busy times for appointments or stays under either confirmation policy; it does not send bookings back or provide a real-time double-booking guarantee.
58
58
 
59
59
  Website authors use the platform Booking runtime and `data-fp-booking` containers. The agent controls surrounding HTML and CSS; the service controls forms, availability, validation, email verification and reservation state. Read the Booking module docs and use their exact starter markup. Do not use the old `data-fp-form="booking"` collector for new forms. Customers review the booking, verify their email and submit; verification creates no inventory hold. A customer management link opens a dedicated page. Viewing a link never accepts, declines or cancels a booking.
60
60
 
@@ -66,10 +66,14 @@ All commands above call the same Booking actions as the workspace and internal a
66
66
 
67
67
  `hours set --file hours.json` takes `{"hours":[{"day":1,"start":"09:00","end":"17:00"}]}` with the current revision/key. The server infers the current user; no target-user flag exists. `activation set --file state.json` takes `{"state":"active"}` (also `disabled` or `archived`). `types save --member-users USER_ID,USER_ID --file type.json` resolves joined members in the shared operation; the complete type value must use `memberResourceIds:[]`. Omit the flag to keep the resource-ID contract. An empty `--member-users ''` clears assigned members.
68
68
 
69
- `connections connect --file setup.json --request-key KEY` takes `{"provider":"google","ownership":"personal"}`. Providers are `google`, `outlook`, `ical`; iCal requires `ownership:"shared"`. Optional `action:"reconnect"` requires the existing `connectionId`. An admin may name `responsibleUserId` to request their joined member's consent; the admin does not receive that member's authorization. Follow the returned workspace URL. The same request appears in the editor status bar and workspace overview. An account-only OAuth return is not complete until calendar selection/binding saves. Failures retain a truthful next step; repeating the same request does not create duplicate notices. `setup dismiss --id REQUEST_ID --revision GENERATION --request-key KEY` hides the request; it does not disconnect or mark connected.
69
+ `connections connect --file setup.json --request-key KEY` takes `{"provider":"google","ownership":"personal"}`. Providers are `google`, `outlook`, `ical`; iCal accepts `ownership:"personal"` for your own member or `ownership:"shared"` for admin-managed spaces. Optional `action:"reconnect"` requires the existing `connectionId`. An admin may name `responsibleUserId` to request their joined member's consent; the admin does not receive that member's authorization. Follow the returned workspace URL. The same request appears in the editor status bar and workspace overview. An account-only OAuth return is not complete until calendar selection/binding saves. Failures retain a truthful next step; repeating the same request does not create duplicate notices. `setup dismiss --id REQUEST_ID --revision GENERATION --request-key KEY` hides the request; it does not disconnect or mark connected.
70
70
 
71
71
  `accounts calendars --id ACCOUNT_ID` returns owned provider calendar choices. `calendars save --record-kind account --id ACCOUNT_ID --file selection.json --request-key KEY` selects one, using `calendarId`, `label`, `resourceIds`, `receivesBookings` and `ownership`. Editing uses `--record-kind calendar --id CONNECTION_ID --account ACCOUNT_ID --revision N`. Use `--setup REQUEST_ID` when completing a returned connection request. Provider identity and consent remain protected by the server.
72
72
 
73
- `connections save` updates an iCal connection's label/resourceIds with the current revision. `connections refresh --record-kind feed|calendar --id CONNECTION_ID --revision N --request-key KEY` executes the server-limited refresh, not a link-only request. Disconnect uses the same record kind/ID/revision plus `--file disconnect.json` containing `{"confirmed":true,"removeBinding":false}`. `accounts disconnect` uses the account ID/revision and the same explicit value. `removeBinding:false` keeps dependent availability unavailable until reconnected; `true` removes that dependency. Existing bookings remain. Never infer that requesting refresh means sync succeeded.
73
+ `connections save` updates an iCal connection's label/resourceIds with the current revision. `connections refresh --record-kind feed|calendar --id CONNECTION_ID --revision N --request-key KEY` executes the server-limited refresh, not a link-only request. Disconnect uses the same record kind/ID/revision plus `--file disconnect.json` containing `{"confirmed":true,"removeBinding":true}`. `accounts disconnect` uses the account ID/revision and the same explicit value. Normal disconnect uses `true` to stop checking that source. Explicit `removeBinding:false` keeps dependent availability unavailable until reconnected. Existing bookings remain. Never infer that requesting refresh means sync succeeded.
74
74
 
75
75
  Only consent/private-link entry uses the secure human handoff. Never pass OAuth codes/tokens or private export URLs through files, CLI arguments or chat. `event-open` takes `{"detailId":"OPAQUE_ID_FROM_OWNER_CALENDAR"}`; opening its URL still requires the connecting human. All ordinary reads preserve role redaction; all mutations preserve stable keys, revisions and the existing two-refreshes-per-15-minutes server allowance.
76
+
77
+ Only the booking type chooses instant confirmation or manual acceptance of customer requests; connecting a calendar never changes that choice. Successful imports revalidate every 15 minutes even when the source advertises a longer cache lifetime. Connection summaries include safe `health` with `status`, `label`, `message` and `usable`. Fresh last-good busy times still apply during refresh or a temporary failure; missing, revoked, disconnected, stale (30 minutes) or uncovered imports keep dependent availability unavailable. Retries keep existing backoff and Retry-After. Imported event titles and private feed URLs never appear in these summaries.
78
+
79
+ Repeated Connect for your existing Google/Microsoft account returns its current calendar choices without replacing its grant or selections. Use Reconnect on the existing account to renew permissions.
package/README.md CHANGED
@@ -169,3 +169,7 @@ tables, scope-changing functions, custom types and unsupported syntax reject.
169
169
  Use the [analytics reference](https://frontpage.host/developers/analytics) for
170
170
  metric definitions and examples. The internal agent and MCP expose full
171
171
  page-detail report tools; this release adds no CLI command. MCP excludes SQL.
172
+
173
+ ## Page screenshots
174
+
175
+ Use `frontpage screenshots capture --path /pricing --device mobile --wait --json` to inspect a built staging page, or `frontpage screenshots get CAPTURE_ID --json` to read a result. The site context comes from `--site` or `frontpage use`. Check the returned revision, coverage and warnings. Private-site images require authenticated reads; captures expire after 30 days. Use the tool when it helps, not on every turn or edit. See [SCREENSHOTS.md](SCREENSHOTS.md) for capture modes, retries, limits and image downloads.
package/SCREENSHOTS.md ADDED
@@ -0,0 +1,29 @@
1
+ # Page screenshots
2
+
3
+ Inspect a rendered staging page with the same capture service used by the internal agent. Capture does not edit or publish the site. Use screenshots when seeing the page helps; no capture is required every turn or after every change.
4
+
5
+ ```sh
6
+ frontpage screenshots capture --site SITE --path /pricing --device mobile --wait --json
7
+ frontpage screenshots capture --site SITE --path / --capture full_page --wait --json
8
+ frontpage screenshots capture --site SITE --path / --capture element --selector '#contact' --wait --json
9
+ frontpage screenshots get CAPTURE_ID --site SITE --json
10
+ ```
11
+
12
+ Options: `--path` defaults to `/`; `--device` is `desktop` (1280×800) or `mobile` (390×844); `--capture` is `viewport` (default), `full_page` or `element`. Only element mode accepts/requires `--selector`, which must match exactly one element. Paths cannot contain query strings, fragments or encoded characters. Full-page capture returns ordered slices up to 24,000px; check `coverage` and `warnings`.
13
+
14
+ `capture` returns an ID and queued/running/complete/failed status. `--wait` polls for up to 180 seconds. A pending result still includes its ID; use `get` to continue. A failed capture returns a nonzero exit status. `--request-key UUID` identifies one request. After an uncertain response, repeat the same input and key shown in the error; do not start another request to recover it. To make a fresh capture after a failed job, use a new key.
15
+
16
+ Results describe built staging content and include its revision. They cannot show unsaved or unbuilt edits. Use `frontpage deploy wait` after a push when you want the new build, then check the returned revision. A revision change during capture fails the request rather than labeling new pixels as the old revision.
17
+
18
+ Requires current site membership and normal CLI plan access (Scale, Enterprise or Internal). Each result is also bound to its requesting user; a capture ID is not permission. Private Frontpage subdomains use server-owned, site-bound access. No cookies, bypass headers, arbitrary origins or operator credentials are accepted. Up to six new captures per site per minute.
19
+
20
+ New site capture images are encrypted in the existing store and delivered through authenticated API paths. In a completed result, `result.images` includes ordered `{url,width,height,top,sizeBytes}` entries. `url` is relative to `https://app.frontpage.host`; fetch it using the same API bearer credential to receive PNG bytes. Never treat the raw encrypted object as an image or publish it as a site asset. Image reads recheck permission and expiry. Results expire after 30 days; the existing reaper cleans expired files and retries failed deletions. Rotation of the server audit key makes older encrypted captures unreadable; request a new capture.
21
+
22
+ ```sh
23
+ # Use an image path returned by screenshots get; keep the API key out of logs.
24
+ curl --fail --header "Authorization: Bearer $FRONTPAGE_API_KEY" \
25
+ 'https://app.frontpage.host/api/v1/sites/SITE/screenshots/CAPTURE_ID?image=0' \
26
+ --output screenshot.png
27
+ ```
28
+
29
+ REST: `POST /api/v1/sites/{slug}/screenshots` with `{path,device,capture,selector?}` and optional `Idempotency-Key` UUID; `GET /api/v1/sites/{slug}/screenshots/{id}` for status/result; the same GET with `?image=N` returns PNG bytes. Images require auth even when the source page is public. MCP screenshot integration is pending; see the MCP handoff rather than assuming the command is already an MCP tool.
package/dist/bin.js CHANGED
@@ -39,6 +39,7 @@ import { runSettings } from "./commands/settings.js";
39
39
  import { runEdit } from "./commands/edit.js";
40
40
  import { runFonts } from "./commands/fonts.js";
41
41
  import { runTokens } from "./commands/tokens.js";
42
+ import { runScreenshots } from "./commands/screenshots.js";
42
43
  import { runHistory } from "./commands/history.js";
43
44
  import { runRevert } from "./commands/revert.js";
44
45
  import { runCompletion } from "./commands/completion.js";
@@ -145,6 +146,8 @@ async function main() {
145
146
  return runFonts(args);
146
147
  case "tokens":
147
148
  return runTokens(args);
149
+ case "screenshots":
150
+ return runScreenshots(args);
148
151
  case "history":
149
152
  return runHistory(args);
150
153
  case "revert":
@@ -0,0 +1 @@
1
+ export declare function runScreenshots(args: string[]): Promise<number>;
@@ -0,0 +1,62 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveSlug } from "../site.js";
3
+ import { assertNoUnknownFlags, hasFlag, takeFlagValue } from "../argv.js";
4
+ import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Get, v1Post } from "../http/client.js";
5
+ import { emitErr, emitOk, isJson } from "../runtime.js";
6
+ const usage = "Usage: frontpage screenshots capture [--path /] [--device desktop|mobile] [--capture viewport|full_page|element] [--selector CSS] [--request-key UUID] [--wait] | get CAPTURE_ID [--site SITE] [--json]";
7
+ export async function runScreenshots(args) {
8
+ try {
9
+ const { slug, rest } = await resolveSlug(args), work = [...rest], action = work.shift();
10
+ const endpoint = `/api/v1/sites/${encodeURIComponent(slug)}/screenshots`;
11
+ let result;
12
+ if (action === "capture") {
13
+ const path = takeFlagValue(work, "--path") ?? "/", device = takeFlagValue(work, "--device") ?? "desktop";
14
+ const capture = takeFlagValue(work, "--capture") ?? "viewport", selector = takeFlagValue(work, "--selector");
15
+ const id = takeFlagValue(work, "--request-key") ?? randomUUID(), wait = hasFlag(work, "--wait");
16
+ assertNoUnknownFlags(work, "screenshots");
17
+ if (work.length || !["desktop", "mobile"].includes(device) || !["viewport", "full_page", "element"].includes(capture)
18
+ || (capture === "element") !== !!selector || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))
19
+ throw new CliHttpError(usage, EXIT_USAGE, 400);
20
+ try {
21
+ result = await v1Post(endpoint, { path, device, capture, ...(selector ? { selector } : {}) }, { idempotencyKey: id, timeoutMs: 30000 });
22
+ }
23
+ catch (error) {
24
+ // The server may already have accepted the request. Give the caller the exact retry key.
25
+ if (error instanceof Error)
26
+ error.message += ` Request key: ${id}. Retry the same input with --request-key ${id}.`;
27
+ throw error;
28
+ }
29
+ if (wait) {
30
+ const deadline = Date.now() + 180000;
31
+ while (["queued", "running"].includes(result.status) && Date.now() < deadline) {
32
+ await new Promise(resolve => setTimeout(resolve, 2000));
33
+ result = await v1Get(`${endpoint}/${result.id}`, { timeoutMs: 15000 });
34
+ }
35
+ }
36
+ }
37
+ else if (action === "get") {
38
+ const id = work.shift();
39
+ assertNoUnknownFlags(work, "screenshots");
40
+ if (!id || work.length)
41
+ throw new CliHttpError(usage, EXIT_USAGE, 400);
42
+ result = await v1Get(`${endpoint}/${encodeURIComponent(id)}`, { timeoutMs: 15000 });
43
+ }
44
+ else
45
+ throw new CliHttpError(usage, EXIT_USAGE, 400);
46
+ if (isJson())
47
+ emitOk("screenshots", { slug, ...result });
48
+ else {
49
+ console.log(`${result.id} ${result.status}${result.revision ? ` revision ${result.revision}` : ""}`);
50
+ if (result.error)
51
+ console.error(result.error);
52
+ for (const image of result.result?.images ?? [])
53
+ console.log(image.url);
54
+ if (["queued", "running"].includes(result.status))
55
+ console.log(`Continue: frontpage screenshots get ${result.id} --site ${slug}`);
56
+ }
57
+ return result.status === "failed" ? EXIT_USAGE : EXIT_OK;
58
+ }
59
+ catch (error) {
60
+ return emitErr("screenshots", error);
61
+ }
62
+ }
@@ -19,6 +19,7 @@ _frontpage_completions() {
19
19
  runs) COMPREPLY=( $(compgen -W "list current wait cancel retry get" -- "\${cur}") ) ;;
20
20
  deploy) COMPREPLY=( $(compgen -W "wait" -- "\${cur}") ) ;;
21
21
  media) COMPREPLY=( $(compgen -W "list upload" -- "\${cur}") ) ;;
22
+ screenshots) COMPREPLY=( $(compgen -W "capture get --path --device --capture --selector --request-key --wait --site --json" -- "\${cur}") ) ;;
22
23
  images) COMPREPLY=( $(compgen -W "generate" -- "\${cur}") ) ;;
23
24
  fonts) COMPREPLY=( $(compgen -W "list search" -- "\${cur}") ) ;;
24
25
  tokens) COMPREPLY=( $(compgen -W "set" -- "\${cur}") ) ;;
@@ -68,6 +69,7 @@ _frontpage() {
68
69
  runs) _values 'subcommand' current wait cancel retry get ;;
69
70
  deploy) _values 'subcommand' wait ;;
70
71
  media) _values 'subcommand' list upload ;;
72
+ screenshots) _values 'subcommand' capture get ;;
71
73
  images) _values 'subcommand' generate ;;
72
74
  fonts) _values 'subcommand' list search ;;
73
75
  tokens) _values 'subcommand' set ;;
@@ -109,6 +111,7 @@ export function generateFishCompletion() {
109
111
  `complete -c frontpage -n "__fish_seen_subcommand_from runs" -a "current wait cancel retry get"`,
110
112
  `complete -c frontpage -n "__fish_seen_subcommand_from deploy" -a "wait"`,
111
113
  `complete -c frontpage -n "__fish_seen_subcommand_from media" -a "list upload"`,
114
+ `complete -c frontpage -n "__fish_seen_subcommand_from screenshots" -a "capture get"`,
112
115
  `complete -c frontpage -n "__fish_seen_subcommand_from images" -a "generate"`,
113
116
  `complete -c frontpage -n "__fish_seen_subcommand_from fonts" -a "list search"`,
114
117
  `complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
@@ -40,6 +40,7 @@ export declare function v1Get<T>(path: string, opts?: {
40
40
  }): Promise<T>;
41
41
  export declare function v1Post<T>(path: string, body?: unknown, opts?: {
42
42
  timeoutMs?: number;
43
+ idempotencyKey?: string;
43
44
  }): Promise<T>;
44
45
  export declare function v1Put<T>(path: string, body?: unknown): Promise<T>;
45
46
  export declare function v1Delete<T>(path: string): Promise<T>;
@@ -89,6 +89,8 @@ async function v1Request(method, path, body, opts) {
89
89
  Authorization: `Bearer ${key}`,
90
90
  Accept: "application/json",
91
91
  };
92
+ if (opts?.idempotencyKey)
93
+ headers["Idempotency-Key"] = opts.idempotencyKey;
92
94
  let payload;
93
95
  if (body !== undefined) {
94
96
  headers["Content-Type"] = "application/json";
package/dist/registry.js CHANGED
@@ -3,6 +3,14 @@
3
3
  * Only shipped Phase 0–5 commands appear here.
4
4
  */
5
5
  export const COMMANDS = [
6
+ {
7
+ name: "screenshots", summary: "Inspect a rendered site page on desktop or mobile",
8
+ usage: "frontpage screenshots capture [--path /] [--device desktop|mobile] [--capture viewport|full_page|element] [--selector CSS] [--request-key UUID] [--wait] | get CAPTURE_ID [--site SITE] [--json]",
9
+ topics: ["site-building", "content"],
10
+ examples: ["frontpage screenshots capture --path /pricing --device mobile --wait --json", "frontpage screenshots get CAPTURE_ID --json"],
11
+ flags: ["--request-key UUID preserves the same request after an uncertain response", "--wait polls for up to 180 seconds; a pending result includes its ID", "Image URLs are authenticated API paths; use the same API key to fetch image/png bytes"],
12
+ dangers: ["Captures built staging pages, not unbuilt edits; check revision and coverage. Results expire after 30 days. Capture does not publish."],
13
+ },
6
14
  {
7
15
  name: "help",
8
16
  summary: "Show available commands",
package/llms.txt CHANGED
@@ -69,3 +69,5 @@ Articles needs no setup. Use `frontpage articles create-section --label "Blog" -
69
69
  Bookings: read packaged [BOOKING.md](./BOOKING.md) and `frontpage modules booking`. Native commands share workspace validation, per-type forms, resource availability, revisions and stable request keys. Calendar consent and private calendar-link entry use a human handoff; ordinary authorized refresh/hours/connection operations run through shared actions; never pass private feed links or OAuth secrets through CLI/agent inputs.
70
70
 
71
71
  Analytics reference: https://frontpage.host/developers/analytics. Filter type=pageview for views; count alone includes all matching events. Days presets are 1/7/30/90; bare dashboard defaults to 7, structured ops to 30. SQL dates belong in SQL; parsed reporting allowlists and site RLS apply (4000 chars, 5s, 500 returned rows). Unsafe functions/types, unrelated tables, recursion and unsupported syntax reject. Funnels count session overlap without enforcing chronological order. MCP excludes raw SQL and has its own dashboard/page-detail tools; the CLI has no new page-detail subcommand.
72
+
73
+ - SCREENSHOTS.md: site-scoped desktop/mobile, full-page and element captures; built revisions, protected images, expiry and request recovery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "Frontpage CLI \u2014 manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "MAIL.md",
16
16
  "CUSTOMER-INBOX.md",
17
17
  "PAYMENTS.md",
18
- "BOOKING.md"
18
+ "BOOKING.md",
19
+ "SCREENSHOTS.md"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "tsc -p tsconfig.json",