frontpage-host 0.3.18 → 0.3.20

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
@@ -119,14 +119,14 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
119
119
  ## Modules & articles
120
120
 
121
121
  - Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`. Do not put a CRM webhook or token in HTML.
122
- - Connect-once modules: Store Stripe connect is `frontpage payments connect`. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. Payments and Store share one Stripe account. Pause Payments through its availability action; generic module writes cannot bypass invoice/recurring stop rules. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). Form destinations (HubSpot / Pipedrive / Privyr): connect once, then attach each form. `frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup`. Token write is **admin**; `request-destination-setup` lights the editor amber bar (**editor** key). Do not use `request-setup forms` for a CRM (no-op). Connecting does not attach every form. Privyr: paste the Incoming Webhook URL from Automations → Lead Sources → Webhooks (`https://www.privyr.com/api/v1/incoming-leads/…`), not a token and not Outgoing Webhooks. HubSpot: Development → Keys → Service keys or Development → Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Full docs: https://frontpage.host/developers/modules#forms-destinations. `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules. Store: https://frontpage.host/developers/store
122
+ - Connect-once modules: Store Stripe connect is `frontpage payments connect`. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. Payments and Store share one Stripe account. Pause Payments through its availability action; generic module writes cannot bypass invoice/recurring stop rules. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|signups get|set` (forms set needs `--instance`; do not target `default`). Form destinations (HubSpot / Pipedrive / Privyr): connect once, then attach each form. `frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup`. Token write is **admin**; `request-destination-setup` lights the editor amber bar (**editor** key). Do not use `request-setup forms` for a CRM (no-op). Connecting does not attach every form. Privyr: paste the Incoming Webhook URL from Automations → Lead Sources → Webhooks (`https://www.privyr.com/api/v1/incoming-leads/…`), not a token and not Outgoing Webhooks. HubSpot: Development → Keys → Service keys or Development → Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Full docs: https://frontpage.host/developers/modules#forms-destinations. `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules. Store: https://frontpage.host/developers/store
123
123
  - Google reviews: platform nodes only (`data-fp-google-reviews` / `data-fp-google-ask` + required `data-fp-requires="google_reviews"`). `data-layout` is `feed` (default), `badge`, or `link`. Never invent stars, quotes, a 1–10, or a Maps URL. Do not pick a Place ID via Autocomplete and do not write `placeId` through `modules config`. Owner-supplied Place ID / Maps URL: `frontpage modules google_reviews connect --place-id …` or `--maps-url …` (server resolves + seeds). Curation: `list-reviews`, `hide --id`, `show --id`, `set-stars`, `set-sort`, `set-weight`, `reset-weights`, `refresh`, `disconnect`. Usage: `frontpage modules google_reviews`. Docs: https://frontpage.host/developers/modules/google-reviews. Ask-widget scores: `frontpage inbox google_reviews`.
124
124
  - Articles needs no setup. `frontpage articles create-section --label "Blog" --route blog` creates an active section. Use `archive-section --section blog` / `restore-section --section blog` to turn it off / on. Draft articles stay unpublished. Bodies use sync under `content/` or `articles content get|set`. The section landing is an owned `src/pages/<section>/index.astro`. Frontpage manages article routing; do not read or edit the managed routing file.
125
125
  - Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
126
126
  - Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
127
127
  - Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Human output prints `sitePath:` first (use that in markup — never raw blob hosts), then optional `url:`. Optional `?w=` on rasters only. **Text/CSV** uploads are inline only — **not** added to the media library and **cannot** be linked via `/__media` (`media upload --json` omits full `content`; use `inbox … --csv` for submission exports).
128
128
  - Generated images: `frontpage images generate --prompt "…"` `[--count N]` — one prompt, 1–4 variations of that same picture. Prints `sitePath:` first. Counts against AI $ and storage. Flag-off environments return 404 (`Image generation is not enabled.`). Do not call xAI / Imagine yourself.
129
- - Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
129
+ - Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for reads. Use type=pageview when counting views; count alone includes every matching event. Custom SQL is parsed/allowlisted and site-scoped by RLS; it rejects unrelated tables, unsafe functions/types, recursive CTEs and unsupported syntax. Keep SQL dates inside SQL (there is no SQL --days flag). Limits: 4000 input characters, five seconds, 500 returned rows. Metric definitions and supported examples: https://frontpage.host/developers/analytics. MCP uses separate structured tools and excludes raw SQL.
130
130
  - People CRM: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|export`. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget. List export is CSV (not a one-person legal dump). Full guide: https://frontpage.host/developers/people
131
131
  - Routines: `frontpage routines list|get|add|set|pause|resume|rm|run|runs`. One routine can hold several hours and several days. `8am,12pm,4pm` on `tue,thu,fri` stores hours `[8, 12, 16]` and weekdays `[2, 4, 5]`. Ask for an IANA timezone. Do not guess UTC. Full guide: https://frontpage.host/developers/routines
132
132
  - Store catalog: `frontpage store items|variants|categories|settings|orders|import`. Integer cents only (`$49.99` is `4999`). `--price-cents` is the usual cost. Optional `--discount-cents` is the sale the buyer pays (must be lower). End a sale with `frontpage store variants update <itemId> --id <variantId> --clear-discount`. This sends discountPriceCents: null and keeps the regular price. Omit both sale flags on update to preserve the current sale. Do not combine --clear-discount with --discount-cents; 0 means free, not removal. Read the item back to verify. Stock: use --stock unlimited on variants update or items stock --set unlimited; zero is sold out. Omit stock on update to preserve it. Rename option types or return to one SKU with items update --file, preserving the surviving variant’s regular price, sale and stock. Create a category before assigning it. Photos are a site path `/__media/uploads/<this-slug>/…` or an HTTPS URL already saved on this site. `frontpage payments connect [--no-open]` opens Frontpage then Stripe. Store always uses Add to cart. Use Payments for standalone Pay now and invoices. Service fee is `settings set-checkout-fee`. The cart starts hidden. Product pages are the default: unless they said they do not want product pages, write one `src/pages/products/item.astro` (or shop) with `data-fp-store-product`. If they said no, skip that file and do not bind `href` or `url`. Description is a small markdown subset; product-page bind is `<div data-fp-store-bind="description">`. Every store control carries `data-fp-requires="store"`. Do not bake titles or prices. Full guide: https://frontpage.host/developers/store
@@ -317,3 +317,7 @@ prepared copy over its exact original. Different uploads with the same display n
317
317
  remain distinct. Existing URLs and published videos are not converted automatically.
318
318
  Upgrade the CLI to receive upload-time preparation. Source path and access denials
319
319
  remain errors; they do not become original-file fallback successes.
320
+
321
+ ## Native Booking
322
+
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.
package/BOOKING.md ADDED
@@ -0,0 +1,79 @@
1
+ # Bookings
2
+
3
+ Use `frontpage modules booking` to read the command list. Booking settings, forms, availability and reservations use the same service as Workspace → Bookings. The CLI uses the site's existing plan and membership rules. Admins configure settings, members/spaces and booking types. Editors manage bookings and blocks. Viewers read availability and manage their own appointment hours and authorized calendar operations through the same shared actions. Consent, private calendar-link entry and private event content require the human workspace flow.
4
+
5
+ Start with `frontpage modules booking status --site SITE --json`. An admin must set the business time zone and availability, publish a booking type with assigned members or spaces, and turn online booking on. A member must already hold a Frontpage account and have joined the site. Use the existing Viewer role when inviting someone who only needs personal availability and calendar connections. Workspace Booking tabs are Calendar, Bookings, Booking types, Spaces, Connections and Settings. Choose joined site members directly in a type; there is no Booking Members screen. Each member sets My appointment hours in Settings for this site. Spaces has its own editor.
6
+
7
+ ## Commands
8
+
9
+ | Command after `frontpage modules booking` | Purpose |
10
+ | --- | --- |
11
+ | `status`, `get`, `set` | Read setup/status, read settings, save complete settings. |
12
+ | `resources list`, `resources save`, `resources catalog` | Read or save internal joined-member availability records and spaces. Creating an existing member resolves its shared record without overwriting hours. |
13
+ | `types list`, `types save`, `types catalog` | Read or save booking types and their customer questions. |
14
+ | `calendar` | Read a bounded calendar range, with optional member/space filters. |
15
+ | `availability` | Read real time slots or stay dates for a published type. |
16
+ | `bookings list`, `bookings get` | Read a page or the current booking details/revision. |
17
+ | `bookings create`, `accept`, `decline`, `cancel`, `withdraw`, `reschedule` | Create or change a reservation. `withdraw` cancels the customer's request; it does not withdraw just a proposed date change. |
18
+ | `blocks get`, `blocks save`, `blocks remove` | Read, save or remove timed, full-day or recurring blocks. |
19
+ | `members list` | Page joined site members as an admin; no invitations or new people. |
20
+ | `hours get`, `hours set` | Read/save the current member's appointment hours. |
21
+ | `activation set` | Pause, resume or archive native Booking with a current revision. |
22
+ | `accounts list`, `accounts calendars`, `accounts disconnect` | Read own accounts/calendar choices or explicitly disconnect an account. |
23
+ | `calendars get`, `calendars save` | Read current calendar selection or save its binding/destination. |
24
+ | `connections list`, `connect`, `save`, `refresh`, `disconnect` | Read safe status, request secure setup, or perform ordinary owned operations. |
25
+ | `setup list`, `setup dismiss` | Read or dismiss a pending human request. |
26
+ | `event-open --file event.json` | Return an owner-authenticated workspace URL; no private event fields. |
27
+
28
+ Use `--site SITE` on every example when the CLI has no selected site. Read operations accept `--limit 1..100` and `--before CURSOR` where supported. Keep the same list filters when using a returned `nextCursor`. Booking lists also accept `--search TEXT` and `--status requested|confirmed|cancelled|declined|expired`.
29
+
30
+ Calendar and availability use `--from ISO_INSTANT --to ISO_INSTANT`, at most 31 days per call. Calendar uses optional `--member ID --space ID`. Availability also needs `--type TYPE_ID`; use `--booking BOOKING_ID` when changing that existing reservation; stays can use `--arrival YYYY-MM-DD`. Returned slots contain exact instants; do not invent a time or infer availability from an empty/error response.
31
+
32
+ ## Writes and retries
33
+
34
+ Save/create/reschedule commands take `--file value.json` containing the complete native value, under 60 KB. Updates also need `--id ID --revision N`; initial settings use `--revision 0`. Read current values before editing. Keep only input fields, not IDs, timestamps, service state or other response metadata. A type owns its own extra form questions; phone is not a site-wide requirement. Name and email remain required.
35
+
36
+ Every write needs `--request-key UNIQUE_KEY` (8–128 letters, digits, `_` or `-`). Reuse the exact key and file after a timeout. Changed input needs a new key. A revision/availability conflict needs fresh reads and user review; do not blindly retry with a newer revision.
37
+
38
+ ```bash
39
+ frontpage modules booking get --site SITE --json
40
+ frontpage modules booking set --site SITE --file settings.json --revision 0 --request-key setup-20261027
41
+ frontpage modules booking resources list --site SITE --json
42
+ frontpage modules booking types list --site SITE --limit 50 --json
43
+ frontpage modules booking availability --site SITE --type TYPE_ID --from 2026-11-02T00:00:00Z --to 2026-11-03T00:00:00Z --json
44
+ frontpage modules booking bookings create --site SITE --file booking.json --request-key request-20261027 --json
45
+ frontpage modules booking bookings get --site SITE --id BOOKING_ID --json
46
+ frontpage modules booking bookings accept --site SITE --id BOOKING_ID --revision 1 --request-key accept-20261027 --json
47
+ ```
48
+
49
+ Appointments use `typeId`, `start`, `customer:{name,email}` and `answers:{}`. Stays use `arrival` and `departure` instead of `start`; the type sets check-in/check-out times and night limits. Optional `memberResourceId` and `spaceResourceId` select preferences from real resources. Rescheduling keeps the booking's saved type, customer and answers. An approval request holds availability until accepted, declined or expired; it is not a confirmed booking. A proposed change can hold new dates while retaining the confirmed original.
50
+
51
+ A recurring block update/removal adds `--scope this|following|all` and the exact returned `--occurrence YYYY-MM-DDTHH:mm` when required. Times follow the block's IANA time zone; full-day ends are exclusive. A single occurrence can change its title and dates, including full-day or timed spans. Its members/spaces and repeat schedule stay with the series; choose the entire series to change them. Don't guess an occurrence from the currently displayed week.
52
+
53
+ ## Connections and the website
54
+
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
+
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
+
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
+
61
+ Private external event details are available only to the connecting human in the workspace Calendar. CLI/agent calendar output remains busy times only; there is no CLI command for private event content.
62
+
63
+ ## Shared setup and connection operations
64
+
65
+ All commands above call the same Booking actions as the workspace and internal agent. Source command aliases and write requirements come from the generated shared contract. Publishing this package is a separate release step; a local source build is not proof of npm availability.
66
+
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
+
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
+
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
+
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
+
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
@@ -150,3 +150,22 @@ prepared copy over its exact original. Different uploads with the same display n
150
150
  remain distinct. Existing URLs and published videos are not converted automatically.
151
151
  Upgrade the CLI to receive upload-time preparation. Source path and access denials
152
152
  remain errors; they do not become original-file fallback successes.
153
+
154
+ 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.
155
+
156
+ ## Analytics
157
+
158
+ `frontpage analytics` reads the existing site-wide dashboard. Use `query`,
159
+ `summary`, `timeseries`, `funnel` and `compare` for structured reports; page filters
160
+ can select one recorded path. For pageview counts include `type: pageview`: a
161
+ plain count includes all matching events. Presets are 1, 7, 30 and 90 days; bare
162
+ dashboard defaults to 7, structured operations to 30. Funnels report session
163
+ overlap, not strictly ordered events.
164
+
165
+ Custom `analytics sql` uses PostgreSQL parsing, reporting-object allowlists, RLS
166
+ and a read-only transaction. Put dates inside SQL. Input is capped at 4000
167
+ characters, database time at five seconds and returned rows at 500. Unrelated
168
+ tables, scope-changing functions, custom types and unsupported syntax reject.
169
+ Use the [analytics reference](https://frontpage.host/developers/analytics) for
170
+ metric definitions and examples. The internal agent and MCP expose full
171
+ page-detail report tools; this release adds no CLI command. MCP excludes SQL.
@@ -1,7 +1,8 @@
1
+ import { runBooking } from "../modules/booking.js";
1
2
  import { resolveSlug } from "../site.js";
2
3
  import { EXIT_OK, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
4
  import { takeFlag } from "../argv.js";
4
- import { emitUsage } from "../runtime.js";
5
+ import { emitErr, emitUsage } from "../runtime.js";
5
6
  import { formatModuleUsage, formatModulesIndex, formatPaymentsIsNotAModule, formatShopIsNotAModule, isCliModuleAction, isDocumentedModuleKey, isGenericModuleVerb, } from "../modules/commands.js";
6
7
  import { runGoogleReviews } from "../modules/google_reviews.js";
7
8
  import { isTypedConfigCliKey, runTypedConfig } from "../modules/typed_config.js";
@@ -48,6 +49,8 @@ export async function runModules(args) {
48
49
  if (isCliModuleAction(peekedKey, peekedAction)) {
49
50
  const { slug } = await resolveSlug(args);
50
51
  const extraFlags = positionalArgs(args).slice(2);
52
+ if (peekedKey === "booking")
53
+ return await runBooking(slug, peekedAction, extraFlags);
51
54
  if (peekedKey === "google_reviews") {
52
55
  return await runGoogleReviews(slug, peekedAction, extraFlags);
53
56
  }
@@ -165,6 +168,9 @@ export async function runModules(args) {
165
168
  ].join("\n"));
166
169
  }
167
170
  catch (err) {
171
+ // Keep Booking domain codes, retry advice and field feedback through the outer command too.
172
+ if (positionalArgs(args)[0] === "booking")
173
+ return emitErr("modules booking", err);
168
174
  console.error(err instanceof Error ? err.message : String(err));
169
175
  return exitCodeForError(err);
170
176
  }
@@ -8,6 +8,11 @@ export type V1Envelope = {
8
8
  code?: string;
9
9
  message?: string;
10
10
  requestId?: string;
11
+ details?: {
12
+ bookingCode?: string;
13
+ retryable?: boolean;
14
+ fieldErrors?: Record<string, string>;
15
+ };
11
16
  };
12
17
  };
13
18
  export declare class CliHttpError extends Error {
@@ -16,9 +21,14 @@ export declare class CliHttpError extends Error {
16
21
  readonly code?: string;
17
22
  /** v1 envelope requestId when the API returned one (§CLI Phase D). */
18
23
  readonly requestId?: string;
24
+ bookingDetails?: {
25
+ bookingCode: string;
26
+ retryable: boolean;
27
+ fieldErrors?: Record<string, string>;
28
+ };
19
29
  constructor(message: string, exitCode: number, status: number, code?: string, requestId?: string);
20
30
  }
21
- /** Map a non-OK v1 response into CliHttpError (shared by JSON + multipart clients). */
31
+ /** Booking preserves its existing service error contract without changing other CLI failures. */
22
32
  export declare function httpErrorFromV1(status: number, parsed: V1Envelope, fallbackMessage?: string): CliHttpError;
23
33
  export declare function apiBase(): string;
24
34
  /** Bearer header for raw fetch (e.g. Blob client-token exchange). */
@@ -14,6 +14,7 @@ export class CliHttpError extends Error {
14
14
  code;
15
15
  /** v1 envelope requestId when the API returned one (§CLI Phase D). */
16
16
  requestId;
17
+ bookingDetails;
17
18
  constructor(message, exitCode, status, code, requestId) {
18
19
  super(message);
19
20
  this.name = "CliHttpError";
@@ -24,7 +25,7 @@ export class CliHttpError extends Error {
24
25
  }
25
26
  }
26
27
  /** Map a non-OK v1 response into CliHttpError (shared by JSON + multipart clients). */
27
- export function httpErrorFromV1(status, parsed, fallbackMessage) {
28
+ function baseHttpErrorFromV1(status, parsed, fallbackMessage) {
28
29
  const message = parsed.error?.message ?? fallbackMessage ?? `Request failed (${status})`;
29
30
  const code = parsed.error?.code;
30
31
  const requestId = parsed.error?.requestId;
@@ -55,6 +56,13 @@ export function httpErrorFromV1(status, parsed, fallbackMessage) {
55
56
  }
56
57
  return new CliHttpError(message, EXIT_UNEXPECTED, status, code, requestId);
57
58
  }
59
+ /** Booking preserves its existing service error contract without changing other CLI failures. */
60
+ export function httpErrorFromV1(status, parsed, fallbackMessage) {
61
+ const error = baseHttpErrorFromV1(status, parsed, fallbackMessage), details = parsed.error?.details;
62
+ if (details && typeof details.bookingCode === "string")
63
+ error.bookingDetails = { bookingCode: details.bookingCode, retryable: details.retryable === true, ...(details.fieldErrors ? { fieldErrors: details.fieldErrors } : {}) };
64
+ return error;
65
+ }
58
66
  export function apiBase() {
59
67
  const fromEnv = process.env.FRONTPAGE_API_BASE?.replace(/\/$/, "");
60
68
  if (fromEnv)
@@ -0,0 +1,130 @@
1
+ {
2
+ "groups": {
3
+ "resources": {
4
+ "list": "resources_list",
5
+ "save": "resource_save",
6
+ "catalog": "resource_catalog"
7
+ },
8
+ "members": {
9
+ "list": "members_list"
10
+ },
11
+ "hours": {
12
+ "get": "appointment_hours_get",
13
+ "set": "appointment_hours_save"
14
+ },
15
+ "activation": {
16
+ "set": "activation_save"
17
+ },
18
+ "accounts": {
19
+ "list": "provider_accounts_list",
20
+ "calendars": "provider_calendars_list",
21
+ "disconnect": "provider_account_disconnect"
22
+ },
23
+ "calendars": {
24
+ "get": "provider_calendar_get",
25
+ "save": "provider_calendar_save"
26
+ },
27
+ "setup": {
28
+ "list": "setup_requests_list",
29
+ "dismiss": "setup_request_dismiss"
30
+ },
31
+ "types": {
32
+ "list": "types_list",
33
+ "save": "type_save",
34
+ "catalog": "type_catalog"
35
+ },
36
+ "bookings": {
37
+ "list": "bookings_list",
38
+ "get": "booking_get",
39
+ "create": "booking_create",
40
+ "accept": "booking_accept",
41
+ "decline": "booking_decline",
42
+ "cancel": "booking_cancel",
43
+ "withdraw": "booking_withdraw",
44
+ "reschedule": "booking_reschedule"
45
+ },
46
+ "blocks": {
47
+ "get": "block_get",
48
+ "save": "block_save",
49
+ "remove": "block_remove"
50
+ },
51
+ "connections": {
52
+ "list": "connections_list",
53
+ "connect": "connect_request",
54
+ "refresh": "connection_refresh",
55
+ "save": "connection_save",
56
+ "disconnect": "connection_disconnect"
57
+ }
58
+ },
59
+ "direct": {
60
+ "status": "status",
61
+ "get": "settings_get",
62
+ "set": "settings_save",
63
+ "calendar": "calendar",
64
+ "availability": "availability",
65
+ "event-open": "event_open"
66
+ },
67
+ "writes": [
68
+ "settings_save",
69
+ "resource_save",
70
+ "type_save",
71
+ "booking_create",
72
+ "booking_accept",
73
+ "booking_decline",
74
+ "booking_cancel",
75
+ "booking_reschedule",
76
+ "booking_withdraw",
77
+ "block_save",
78
+ "block_remove",
79
+ "connection_refresh",
80
+ "connect_request",
81
+ "activation_save",
82
+ "appointment_hours_save",
83
+ "provider_calendar_save",
84
+ "provider_account_disconnect",
85
+ "connection_save",
86
+ "connection_disconnect",
87
+ "setup_request_dismiss"
88
+ ],
89
+ "actions": [
90
+ "status",
91
+ "settings_get",
92
+ "settings_save",
93
+ "resources_list",
94
+ "resource_save",
95
+ "types_list",
96
+ "type_save",
97
+ "calendar",
98
+ "availability",
99
+ "bookings_list",
100
+ "booking_get",
101
+ "booking_create",
102
+ "booking_accept",
103
+ "booking_decline",
104
+ "booking_cancel",
105
+ "booking_reschedule",
106
+ "booking_withdraw",
107
+ "block_get",
108
+ "block_save",
109
+ "block_remove",
110
+ "connections_list",
111
+ "connection_refresh",
112
+ "connect_request",
113
+ "activation_save",
114
+ "members_list",
115
+ "resource_catalog",
116
+ "type_catalog",
117
+ "appointment_hours_get",
118
+ "appointment_hours_save",
119
+ "provider_accounts_list",
120
+ "provider_calendars_list",
121
+ "provider_calendar_get",
122
+ "provider_calendar_save",
123
+ "provider_account_disconnect",
124
+ "connection_save",
125
+ "connection_disconnect",
126
+ "event_open",
127
+ "setup_requests_list",
128
+ "setup_request_dismiss"
129
+ ]
130
+ }
@@ -0,0 +1,2 @@
1
+ export declare const BOOKING_CLI_ACTIONS: string[];
2
+ export declare function runBooking(slug: string, verb: string, args: string[]): Promise<number>;
@@ -0,0 +1,91 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { assertNoUnknownFlags, takeFlagValue } from "../argv.js";
3
+ import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
4
+ import { emitOk, isJson } from "../runtime.js";
5
+ import contract from "./booking-contract.generated.json" with { type: "json" };
6
+ const groups = contract.groups;
7
+ export const BOOKING_CLI_ACTIONS = [...Object.keys(contract.groups), ...Object.keys(contract.direct)];
8
+ export async function runBooking(slug, verb, args) {
9
+ const rest = [...args], action = groups[verb]
10
+ ? groups[verb][rest.shift() || "list"]
11
+ : contract.direct[verb];
12
+ if (!action)
13
+ throw new CliHttpError("Unknown Booking action. Run: frontpage modules booking", EXIT_USAGE, 0);
14
+ const id = takeFlagValue(rest, "--id"), file = takeFlagValue(rest, "--file"), revision = takeFlagValue(rest, "--revision"), requestKey = takeFlagValue(rest, "--request-key"), scope = takeFlagValue(rest, "--scope"), occurrence = takeFlagValue(rest, "--occurrence"), recordKind = takeFlagValue(rest, "--record-kind"), accountId = takeFlagValue(rest, "--account"), setupRequestId = takeFlagValue(rest, "--setup"), memberUsers = takeFlagValue(rest, "--member-users");
15
+ const query = {};
16
+ for (const [flag, key] of [
17
+ ["--from", "from"],
18
+ ["--to", "to"],
19
+ ["--type", "typeId"],
20
+ ["--booking", "bookingId"],
21
+ ["--member", "memberResourceId"],
22
+ ["--space", "spaceResourceId"],
23
+ ["--arrival", "arrival"],
24
+ ["--before", "before"],
25
+ ["--limit", "limit"],
26
+ ["--search", "search"],
27
+ ["--status", "status"],
28
+ ]) {
29
+ const value = takeFlagValue(rest, flag);
30
+ if (value !== undefined)
31
+ query[key] = key === "limit" ? Number(value) : value;
32
+ }
33
+ if (action === "calendar") {
34
+ if (query.memberResourceId) {
35
+ query.memberId = query.memberResourceId;
36
+ delete query.memberResourceId;
37
+ }
38
+ if (query.spaceResourceId) {
39
+ query.spaceId = query.spaceResourceId;
40
+ delete query.spaceResourceId;
41
+ }
42
+ }
43
+ assertNoUnknownFlags(rest, "modules");
44
+ if (rest.length)
45
+ throw new CliHttpError(`Unexpected Booking argument: ${rest[0]}`, EXIT_USAGE, 0);
46
+ const writes = contract.writes.includes(action);
47
+ if (writes && (!requestKey || !/^[A-Za-z0-9_-]{8,128}$/.test(requestKey)))
48
+ throw new CliHttpError("Writes require --request-key (8–128 letters/digits/_/-). Reuse exactly this key and input after a timeout.", EXIT_USAGE, 0);
49
+ let value;
50
+ if (file) {
51
+ const info = await stat(file);
52
+ if (!info.isFile() || info.size > 60000)
53
+ throw new CliHttpError("Booking --file must be a JSON file under 60 KB.", EXIT_USAGE, 0);
54
+ try {
55
+ value = JSON.parse(await readFile(file, "utf8"));
56
+ }
57
+ catch {
58
+ throw new CliHttpError("Booking --file must contain valid JSON.", EXIT_USAGE, 0);
59
+ }
60
+ }
61
+ if ((action.endsWith("_save") ||
62
+ action === "booking_create" ||
63
+ action === "booking_reschedule") &&
64
+ value === undefined)
65
+ throw new CliHttpError("This action requires --file with the complete Booking value.", EXIT_USAGE, 0);
66
+ if (revision !== undefined &&
67
+ (!Number.isSafeInteger(Number(revision)) || Number(revision) < 0))
68
+ throw new CliHttpError("--revision must be a non-negative integer.", EXIT_USAGE, 0);
69
+ const input = {
70
+ action,
71
+ ...(id ? { id } : {}),
72
+ ...(recordKind ? { recordKind } : {}),
73
+ ...(accountId ? { accountId } : {}),
74
+ ...(setupRequestId ? { setupRequestId } : {}),
75
+ ...(memberUsers !== undefined
76
+ ? { memberUserIds: memberUsers ? memberUsers.split(",") : [] }
77
+ : {}),
78
+ ...(value === undefined ? {} : { value }),
79
+ ...(revision === undefined ? {} : { expectedRevision: Number(revision) }),
80
+ ...(requestKey ? { requestKey } : {}),
81
+ ...(scope ? { scope } : {}),
82
+ ...(occurrence ? { occurrence } : {}),
83
+ ...(Object.keys(query).length ? { query } : {}),
84
+ };
85
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/booking`, input, { timeoutMs: 25000 });
86
+ if (isJson())
87
+ emitOk("modules booking", data);
88
+ else
89
+ console.log(JSON.stringify(data, null, 2));
90
+ return EXIT_OK;
91
+ }
@@ -1,3 +1,4 @@
1
+ import bookingContract from "./booking-contract.generated.json" with { type: "json" };
1
2
  /**
2
3
  * §CLI-MOD-FW — per-key CLI registry (usage printer + help --json).
3
4
  * Instagram is not documented here. Google reviews extras ship in Phase 1.
@@ -92,21 +93,35 @@ export const MODULE_COMMANDS = [
92
93
  {
93
94
  key: "booking",
94
95
  name: "Booking",
95
- summary: "Booking requests. Typed get/set for notify email and hours.",
96
- genericActions: GENERIC_ACTIONS,
96
+ summary: "Appointments, stays, per-type forms and calendar availability.",
97
+ genericActions: ["docs"],
97
98
  extraActions: [
99
+ "status",
98
100
  "get",
99
- "set [--notify-email] [--hours \"…\"]",
101
+ "set --file settings.json --revision N --request-key KEY",
102
+ "resources list|save|catalog", "members list", "hours get|set", "activation set", "accounts list|calendars|disconnect", "calendars get|save", "setup list|dismiss", "event-open --file event.json",
103
+ "types list|save|catalog",
104
+ "bookings list|get|create|accept|decline|cancel|reschedule|withdraw",
105
+ "blocks get|save|remove",
106
+ "connections list|connect|save|refresh|disconnect",
107
+ "calendar --from ISO --to ISO [--member ID] [--space ID]",
108
+ "availability --type ID --from ISO --to ISO [--arrival YYYY-MM-DD] [--booking ID]",
100
109
  ],
101
110
  notYet: [],
102
111
  examples: [
103
- "frontpage modules booking",
112
+ "frontpage modules booking status",
104
113
  "frontpage modules booking get",
105
- "frontpage modules booking set --notify-email book@example.com --hours \"Mon–Fri 9–5\"",
114
+ "frontpage modules booking types list --limit 50",
115
+ "frontpage modules booking types save --file type.json --request-key type-create-unique-1",
116
+ "frontpage modules booking bookings accept --id <id> --revision 2 --request-key accept-unique-1",
117
+ "frontpage modules booking connections connect --file setup.json --request-key setup-request-unique-1",
106
118
  "frontpage modules booking docs",
107
119
  ],
108
120
  notes: [
109
- "Typed flags write the same whitelist as `modules config`. Generic JSON config still works.",
121
+ "Use --file for a complete settings/resource/type/booking/block value. Updates need --id and --revision. First settings save uses --revision 0.",
122
+ "Every write requires --request-key. Reuse its exact body/key after timeout; changed input needs a new key. List responses return nextCursor; pass it as --before with the same list/site and filters. Limit is at most 100. Booking lists accept --search and --status.",
123
+ "Calendar ranges are at most 31 days. --member/--space filter both together. Blocks support --scope this|following|all and the exact --occurrence returned by the calendar.",
124
+ "Connections connect/refresh return a human workspace link. Calendar consent and private calendar settings stay with the connecting human. No secrets go through the CLI.",
110
125
  ],
111
126
  },
112
127
  {
@@ -218,7 +233,9 @@ export function isCliModuleAction(key, action) {
218
233
  "request-destination-setup",
219
234
  ].includes(action));
220
235
  }
221
- if (key === "callbar" || key === "booking" || key === "signups") {
236
+ if (key === "booking")
237
+ return Object.hasOwn(bookingContract.groups, action) || Object.hasOwn(bookingContract.direct, action);
238
+ if (key === "callbar" || key === "signups") {
222
239
  return TYPED_CONFIG_CLI_ACTIONS.includes(action);
223
240
  }
224
241
  return false;
package/dist/registry.js CHANGED
@@ -308,10 +308,10 @@ export const COMMANDS = [
308
308
  examples: [
309
309
  "frontpage analytics --days 7",
310
310
  "frontpage analytics summary --days 30",
311
- "frontpage analytics query --dimension path --measure count --limit 10",
311
+ "frontpage analytics query --dimension path --measure count --filters '{\"type\":\"pageview\"}' --limit 10",
312
312
  `frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]'`,
313
313
  "frontpage analytics funnel --steps /, /pricing, /updates",
314
- "frontpage analytics timeseries --metric pageviews --interval day --days 14",
314
+ "frontpage analytics timeseries --metric pageviews --interval day --days 7",
315
315
  `frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}'`,
316
316
  'frontpage analytics sql --sql "SELECT path, count(*)::int AS n FROM analytics_events GROUP BY 1 ORDER BY n DESC LIMIT 10"',
317
317
  ],
@@ -320,7 +320,13 @@ export const COMMANDS = [
320
320
  "funnel --steps accepts JSON array or comma-separated paths (/, /pricing)",
321
321
  "Invalid JSON for --steps / --a / --b prints an explicit parse error (not bare Usage)",
322
322
  "Unknown leftover flags fail closed (exit 2)",
323
- "Ops use POST …/analytics with allowlisted dimensions/measures; SQL is guard-validated + RLS",
323
+ "Ops use POST …/analytics with allowlisted dimensions/measures; custom SQL is parsed, allowlisted and RLS site-scoped",
324
+ "Days presets: 1, 7, 30, 90. Bare dashboard defaults to 7; other structured ops default to 30",
325
+ "For pageviews filter type=pageview; count without that filter includes all matching events",
326
+ "SQL: one validated reporting SELECT tree; 4000 input chars, 5s timeout, 500 returned rows; dates belong inside SQL",
327
+ "Custom functions/types, unrelated/system tables, scope changes, recursive CTEs and unknown syntax reject",
328
+ "Funnel counts sessions matching successive step sets; it does not require chronological event order",
329
+ "Metrics and SQL reference: https://frontpage.host/developers/analytics; MCP has separate tools and no raw SQL",
324
330
  ],
325
331
  },
326
332
  {
package/dist/runtime.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Process-global CLI runtime flags (set once from argv in bin.ts).
3
+ */
4
+ import { CliHttpError } from "./http/client.js";
1
5
  export declare function setRuntimeFlags(flags: {
2
6
  json?: boolean;
3
7
  quiet?: boolean;
@@ -14,6 +18,7 @@ export type JsonErrorBody = {
14
18
  httpStatus?: number;
15
19
  exitCode: number;
16
20
  requestId?: string;
21
+ details?: CliHttpError["bookingDetails"];
17
22
  };
18
23
  export declare function emitOk(command: string, data: unknown): void;
19
24
  /** Structured failure with optional data payload (e.g. failed run wait). */
@@ -24,6 +29,7 @@ export declare function emitFail(command: string, args: {
24
29
  requestId?: string;
25
30
  httpStatus?: number;
26
31
  data?: unknown;
32
+ details?: CliHttpError["bookingDetails"];
27
33
  }): number;
28
34
  export declare function emitErr(command: string, err: unknown): number;
29
35
  export declare function emitUsage(command: string, message: string, exitCode?: number): number;
package/dist/runtime.js CHANGED
@@ -43,6 +43,8 @@ export function emitFail(command, args) {
43
43
  body.httpStatus = args.httpStatus;
44
44
  if (args.requestId)
45
45
  body.requestId = args.requestId;
46
+ if (args.details)
47
+ body.details = args.details;
46
48
  if (jsonMode) {
47
49
  const out = { ok: false, command, error: body };
48
50
  if (args.data !== undefined)
@@ -65,6 +67,7 @@ export function emitErr(command, err) {
65
67
  code: err.code,
66
68
  httpStatus: err.status || undefined,
67
69
  requestId: err.requestId,
70
+ details: err.bookingDetails,
68
71
  });
69
72
  }
70
73
  return emitFail(command, { message, exitCode });
package/llms.txt CHANGED
@@ -65,3 +65,7 @@ Video uploads: the current CLI prepares a browser-compatible MP4 after uploading
65
65
  AI Support Agent knowledge: read the latest note, then save the same ID. Last save wins and prior saved changes remain in history. Knowledge history supports limit/cursor and version reads within the plan Rewind allowance plus the latest version. Archive with customer-inbox post knowledge/ID/archive and {} to stop future use without deleting records. See CUSTOMER-INBOX.md.
66
66
 
67
67
  Articles needs no setup. Use `frontpage articles create-section --label "Blog" --route blog` to create an active section, `archive-section --section blog` / `restore-section --section blog` to turn it off / on, and `create-article` or `content get|set` for content. Draft articles stay unpublished. Frontpage manages article routing; do not read or edit the managed routing file.
68
+
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
+
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "PEOPLE.md",
15
15
  "MAIL.md",
16
16
  "CUSTOMER-INBOX.md",
17
- "PAYMENTS.md"
17
+ "PAYMENTS.md",
18
+ "BOOKING.md"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "tsc -p tsconfig.json",