frontpage-host 0.3.15 → 0.3.17

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
@@ -282,6 +282,8 @@ AI Support Agent team guidance: administrators may set `guidanceEmail` through t
282
282
 
283
283
  ## Emails workspace workflows
284
284
 
285
+ Campaign timing uses the existing `mail save --file` and `mail campaign preview|send|get|unschedule|reschedule` commands. Read [MAIL.md, Campaign scheduling](./MAIL.md#campaign-scheduling) before using it. Save document.scheduledAt as a future ISO timestamp with an explicit timezone, null for Send now, or omit to retain it. Saving never confirms sending; overdue drafts remain drafts. Use document.scheduleLocal:{dateTime,timeZone} for local YYYY-MM-DDTHH:mm plus IANA timezone; the server converts it. ISO scheduledAt remains supported. Do not supply both. Review the post-save content timestamp, exact audience, eligible count and saved schedule before campaign send with a stable requestKey. Confirmed groups/filters resolve once, and normal delivery restrictions remain. The owner agent uses save_mail_editor then manage_people area:campaign; MCP uses save_mail_editor and campaign.preview/send/get. Direct saved-email sends cannot bypass a dated campaign. Reuse exact requests after uncertain responses. Before queueing, unschedule cancels a scheduled email to Draft, keeping its email/audience; reschedule changes only time and retains confirmed recipients/content. Both require documentUpdatedAt from a fresh get and a stable requestKey. Reschedule accepts scheduleLocal or scheduledAt. Read MAIL.md for examples.
286
+
285
287
  See [MAIL.md](./MAIL.md) for the complete save contract, reusable people picker directory, saved campaign recipients, Off-series enrollment and resume timing, all sender settings/null resets, suppression rules, Sent queries and details, and retry-safe examples. `frontpage mail save --file save.json` saves template and pending series together; it never sends. New series default On; existing series retain their saved state. Save email steps before enrolling anyone. Check the returned series status before attaching it to a rule; an Off series holds enrollment until turned On. `frontpage mail people` reads the same site-scoped directory as the workspace. Use `frontpage help mail --json` for current commands and flags. No new CLI binary, API namespace or migration command is required. Hosted guides and article help must be updated through the Frontpage CLI.
286
288
 
287
289
  Rule placement: read a rule revision, then use `people workspace rules.place` with `id`, `owner` and `expectedRevision`. This only organizes the rule; it does not change its trigger or execution. New UI rules/attachments save paused. Generic attachment calls still need explicit `enabled:false` for paused creation. Saved optional conditions and exact duration minutes remain supported through CLI and agent calls.
package/MAIL.md CHANGED
@@ -100,6 +100,114 @@ Include `audience` with a one_off document in the same `mail save` request. Supp
100
100
 
101
101
  For an audience-only change, the existing `mail campaign audience --id DOCUMENT_ID --file audience.json` remains supported. Its JSON is `{"audience":{"personIds":["PERSON_ID"]}}`, or `{"audience":null}` to clear. Saved selection is intent; final send review resolves groups and filters again, checks permission and delivery restrictions, and freezes the actual recipients and content. Started campaigns cannot be edited or sent twice with a new key. Read `PEOPLE.md` for preview/send review and stable send keys.
102
102
 
103
+ ## Campaign scheduling
104
+
105
+ The workspace, CLI, owner agent and MCP use the same saved campaign and confirmation service. Send now is the default. Saving a date never confirms sending: a draft stays a draft even after that date passes. Only an explicit campaign confirmation makes it eligible for the worker. The worker selects confirmed (`pending`) campaigns whose scheduled time has arrived, or whose scheduled time is null. Its existing minute pass and delivery checks can delay sending; the selected time is the earliest send time, not a delivery guarantee.
106
+
107
+ | Action | CLI | Owner agent | MCP |
108
+ | --- | --- | --- | --- |
109
+ | Read content | `mail email get ID --json` | `get_mail_email` | `frontpage_mail_read`, operation `get_mail_email` |
110
+ | Read timezone | `mail settings get --json` | `set_mail_settings({})` (read only) | `frontpage_mail_read`, operation `get_mail_settings` |
111
+ | Save draft, audience and timing | `mail save --file draft.json` | `save_mail_editor` | `frontpage_mail_write`, operation `save_mail_editor` |
112
+ | Review audience | `mail campaign preview --file audience.json` | `manage_people`, area `campaign`, operation `preview` | `frontpage_mail_read`, operation `campaign.preview` |
113
+ | Confirm sending or scheduling | `mail campaign send --id ID --file review.json --request-key KEY` | `manage_people`, area `campaign`, operation `send` | `frontpage_mail_write`, operation `campaign.send` |
114
+ | Cancel a scheduled email | `mail campaign unschedule --id ID --file cancel.json --request-key KEY` | `manage_people`, area `campaign`, operation `unschedule` | `frontpage_mail_write`, operation `campaign.unschedule` |
115
+ | Change a confirmed time | `mail campaign reschedule --id ID --file time.json --request-key KEY` | `manage_people`, area `campaign`, operation `reschedule` | `frontpage_mail_write`, operation `campaign.reschedule` |
116
+ | Read saved audience, timing and status | `mail campaign get --id ID --json` | `manage_people`, area `campaign`, operation `get` | `frontpage_mail_read`, operation `campaign.get` |
117
+
118
+ Use `--site YOUR_SITE --json` with each CLI command. Agent calls bind the current site; never supply a siteId. MCP calls take `slug`, `operation` and `payload`, with the same fields inside payload. Existing role, plan, consent and delivery checks apply.
119
+
120
+ ### Save a schedule without sending
121
+
122
+ Read the current campaign first. The workspace uses the viewer's local timezone. Tools can save `document.scheduleLocal:{dateTime:"2030-11-15T09:00",timeZone:"America/Chicago"}`; the server converts local time to UTC. Use an IANA timezone, not a guessed offset. Missing clock-change times are rejected; repeated clock-change times use the earlier occurrence. Ask for the owner's timezone if unknown. Existing `document.scheduledAt` ISO timestamps with `Z` or an explicit offset still work. Supply only one timing input. Omit both to retain timing; scheduledAt:null selects Send now.
123
+
124
+ - A timestamp selects Schedule for later.
125
+ - `null` selects Send now and clears the saved date. It still does not send.
126
+ - Omitting the field preserves existing timing.
127
+ - Templates and series do not accept this field.
128
+
129
+ Example `draft.json` for an existing campaign; replace IDs, timestamps, content and audience with the current reviewed values. Omit id and expectedUpdatedAt when creating a new campaign. Choose a future date for the actual request.
130
+
131
+ ```json
132
+ {
133
+ "requestKey": "UNIQUE_SAVE_KEY",
134
+ "document": {
135
+ "id": "CAMPAIGN_DOCUMENT_ID",
136
+ "expectedUpdatedAt": "EXACT_CURRENT_DOCUMENT_TIMESTAMP",
137
+ "kind": "one_off",
138
+ "name": "November update",
139
+ "subject": "Your November update",
140
+ "preheader": "News from our team.",
141
+ "blocks": [{"type":"paragraph","text":"Here is our latest news."}],
142
+ "scheduledAt": "2030-11-15T09:00:00-06:00"
143
+ },
144
+ "audience": {"personIds":["PERSON_ID"]}
145
+ }
146
+ ```
147
+
148
+ ```sh
149
+ frontpage mail save --file draft.json --site YOUR_SITE --json
150
+ frontpage mail email get CAMPAIGN_DOCUMENT_ID --site YOUR_SITE --json
151
+ frontpage mail campaign get --id CAMPAIGN_DOCUMENT_ID --site YOUR_SITE --json
152
+ frontpage mail campaign preview --file audience.json --site YOUR_SITE --json
153
+ ```
154
+
155
+ `audience.json` contains `{"audience":{"personIds":["PERSON_ID"]}}`. Supported choices also include `groupIds`, `stageIds`, `filters`, and `addresses`. The UI's Choose people option uses personIds; List individual email addresses uses addresses. Read `campaign get` to reuse a saved selection. When changing audience sources, send only the new source rather than merging the old selection. Use `audience:null` in `mail save` to clear it. Clearing or replacing an audience does not clear the saved schedule. Explicit addresses do not create People records or grant email permission.
156
+
157
+ ### Review and confirm
158
+
159
+ Show the owner the content, eligible recipient count, and send date/time in the business timezone before confirmation. Fetch current content and campaign state after saving, and preview the exact audience. The confirmation file `review.json` carries those reviewed values:
160
+
161
+ ```json
162
+ {
163
+ "review": {
164
+ "audience": {"personIds":["PERSON_ID"]},
165
+ "eligible": 1,
166
+ "documentUpdatedAt": "EXACT_POST_SAVE_DOCUMENT_TIMESTAMP",
167
+ "scheduledAt": "2030-11-15T15:00:00.000Z"
168
+ }
169
+ }
170
+ ```
171
+
172
+ ```sh
173
+ # This is the confirmation: it authorizes sending when due.
174
+ frontpage mail campaign send --id CAMPAIGN_DOCUMENT_ID --file review.json --request-key UNIQUE_SEND_KEY --site YOUR_SITE --json
175
+ frontpage mail campaign get --id CAMPAIGN_DOCUMENT_ID --site YOUR_SITE --json
176
+ ```
177
+
178
+ Copy the canonical UTC scheduledAt from the saved response into review, or null for Send now. Use the exact post-save document timestamp and preview's eligible count; never use a timestamp from before an audience save. The service rejects a changed reviewed audience, count, content timestamp or scheduled time. A past date needs a new future date or Send now before confirmation. Existing older callers without review fields remain supported; agents should always supply the complete review.
179
+
180
+ Confirmation freezes the eligible recipients and content. Groups and filters resolve at confirmation, not at the future send time. Permission, opt-out and delivery checks still apply before sending. A confirmed future campaign has stored status `pending` and scheduledAt; the UI labels that combination Scheduled. Drafts remain `draft` regardless of date. Queued, sent and delivered are different states; read campaign counts and the send ledger. Before queueing begins, a confirmed schedule can be cancelled or moved as described below. Its content and recipients stay fixed unless cancelled and confirmed again.
181
+
182
+ Keep the same requestKey and exact payload after an uncertain save or confirmation. Read back campaign state and retry the original request; never create a new send key to recover an uncertain result. A changed request needs a fresh review. The older `mail send email` / `send_mail_email` path sends immediately and cannot confirm a scheduled campaign; it returns a conflict for dated campaigns. Use campaign send instead. Test sends and dry runs do not confirm or start the campaign; existing test restrictions still apply.
183
+
184
+ ### Manage a confirmed schedule
185
+
186
+ Read `campaign get` immediately before reviewing a change. It returns `documentUpdatedAt` and `canManageSchedule`. **Cancel scheduled email** uses `unschedule`: it changes status to `draft`, clears scheduledAt and the old confirmed snapshot/recipients, and keeps the email and saved audience. Normal editing resumes. Saving that draft never sends it; sending or scheduling again needs a fresh review and explicit confirmation with a new send request key. There is no cancelled campaign state.
187
+
188
+ `reschedule` changes only time. Show the saved email, recipient count, current time and proposed time, then get the owner's approval. It retains the confirmed recipients/content even if groups have changed. Closing the review makes no change.
189
+
190
+ Both actions work only while the campaign is confirmed and dated, before its first queue batch. They may still work after the due time if queueing has not begun. Once queueing begins, the service rejects them with 409. Refresh and show the saved state; do not substitute a new timestamp without a fresh review.
191
+
192
+ `cancel.json`:
193
+ ```json
194
+ {"documentUpdatedAt":"POST_READ_TIMESTAMP"}
195
+ ```
196
+ `time.json`:
197
+ ```json
198
+ {"documentUpdatedAt":"POST_READ_TIMESTAMP","scheduleLocal":{"dateTime":"2030-11-16T09:00","timeZone":"America/Chicago"}}
199
+ ```
200
+ ```sh
201
+ frontpage mail campaign unschedule --id CAMPAIGN_DOCUMENT_ID --file cancel.json --request-key CANCEL_KEY --site YOUR_SITE --json
202
+ frontpage mail campaign reschedule --id CAMPAIGN_DOCUMENT_ID --file time.json --request-key CHANGE_KEY --site YOUR_SITE --json
203
+ ```
204
+
205
+ For reschedule, scheduledAt is also accepted instead of scheduleLocal. Keep the original key and exact payload after a lost response, then read back state. Accepted action keys cannot reapply an older change over a newer one. An old send confirmation retry cannot restart a cancelled draft. A different action needs a new request key. These operations share the worker's campaign lock and add no table, campaign state or worker.
206
+
207
+ ### Email capability release check
208
+
209
+ For each changed Email capability, check its shared service, UI, public API, CLI help and examples, owner-agent schema/instructions, and MCP operation/docs. Add focused contract checks and test the real adapters. Run `node scripts/people/sync-agent-docs.mjs` after editing the packaged manuals and `node scripts/people/sync-agent-docs.mjs --check` before release. Publish hosted guides through the Frontpage CLI only, then verify the public page. Do not maintain a separate scheduling implementation or a second internal manual.
210
+
103
211
  ## People directory and enrollment
104
212
 
105
213
  ```bash
package/PEOPLE.md CHANGED
@@ -577,6 +577,10 @@ frontpage mail campaign get --id DRAFT_ID --site YOUR_SITE --json
577
577
 
578
578
  Preview reports matched, eligible, no-email, suppressed, and permission-missing counts and sends nothing. Sending freezes content and eligible recipients, deduplicates email addresses, and uses the existing paced email queue. Reuse the send request key after a lost response. Check `campaign get` for queued, sent, pending, failed, and skipped work; queued does not mean delivered.
579
579
 
580
+ Campaigns can also schedule a future send through `mail save` with `document.scheduledAt`. A date on a draft never sends it. After reviewing content, audience and time, `mail campaign send` confirms the saved schedule; the existing worker waits until due. Send now uses scheduledAt:null. `campaign get` returns stored status and scheduledAt; pending plus a future time displays Scheduled in the UI. Groups and filters resolve when confirmed. Read [MAIL.md, Campaign scheduling](./MAIL.md#campaign-scheduling) for complete save, review, retry, agent and MCP examples. Direct `mail send email` is for immediate unscheduled sends and cannot bypass a saved schedule.
581
+
582
+ For the owner agent, use `save_mail_editor` with a one_off document and `scheduleLocal:{dateTime:"2030-11-15T09:00",timeZone:"America/Chicago"}`. The server converts local time to UTC. Use the owner's timezone; ask if unknown. Future ISO scheduledAt including Z or an explicit offset remains supported; do not supply both. Omission preserves timing; null clears it. Read `get_mail_email` after saving and `manage_people` with area:campaign, operation:get for saved timing/audience, then operation:preview for the exact audience and eligible count. Show the owner the content, recipients and local send time. On explicit confirmation, call `manage_people` with area:campaign, operation:send, payload:{documentId,requestKey,review:{audience,eligible,documentUpdatedAt,scheduledAt}}. Use the post-save document timestamp, preview count and canonical saved UTC time. These values must still match; a changed review needs a fresh read and approval. Keep the original requestKey and payload after an uncertain result and read back state before retrying. Never use direct send_mail_email to bypass timing. MCP uses `frontpage_mail_write` operation:save_mail_editor and campaign.send, with campaign.get/preview through `frontpage_mail_read`. Before queueing, campaign.unschedule cancels the scheduled email to Draft; campaign.reschedule changes only time. Both use documentUpdatedAt from a fresh campaign.get and a stable requestKey. Reschedule takes scheduleLocal or scheduledAt. Get approval for the new time and retain confirmed recipients/content. Cancellation keeps the email/audience for editing; scheduling again needs fresh confirmation and a new send key. Old accepted retries cannot undo later actions. UI, CLI, agent and MCP share this service. See the Mail guide for examples.
583
+
580
584
  ## CLI, API, and agent reference
581
585
 
582
586
  The editor, public API, CLI, and site agent call the same application service. Every operation below is available without opening the UI.
@@ -124,7 +124,7 @@ export async function runCampaign(slug, args) {
124
124
  ...payload,
125
125
  operation,
126
126
  ...(documentId ? { documentId } : {}),
127
- ...(operation === "send"
127
+ ...(["send", "unschedule", "reschedule"].includes(operation)
128
128
  ? { requestKey: requestKey ?? payload.requestKey }
129
129
  : {}),
130
130
  });
package/dist/registry.js CHANGED
@@ -591,6 +591,8 @@ export const COMMANDS = [
591
591
  "frontpage mail series steps set <id> --file steps.json",
592
592
  "frontpage mail template history <id> --limit 20 --json",
593
593
  "frontpage mail template history-get <id> --history-id <saved-copy-id> --json",
594
+ "frontpage mail save --file scheduled-draft.json --site YOUR_SITE --json",
595
+ "frontpage mail campaign send --id DRAFT_ID --file review.json --request-key SEND_KEY --site YOUR_SITE --json",
594
596
  "frontpage mail suppress add --email bounce@example.com",
595
597
  ],
596
598
  flags: [
@@ -599,7 +601,8 @@ export const COMMANDS = [
599
601
  "settings set --file settings.json supports all eight fields and null resets; --header-align, --window-start, --window-end, --hourly-cap null|1..1000.",
600
602
  "sends list --q --status --type campaign|series|confirmation|template|test|inline --page 1 --limit 20.",
601
603
  "Series status active means On; paused/draft mean Off. Enrollment while Off waits at step 1, without sending. Existing people resume their next unsent step.",
602
- "campaign preview|audience|send|get. --id DRAFT_ID, --file audience.json ({audience:{groupIds|stageIds|personIds|filters}}). send requires --request-key; reuse it after a network error. Templates/series are for automation; campaigns have separate drafts. See PEOPLE.md.",
604
+ "campaign preview|audience|send|get|unschedule|reschedule. --id DRAFT_ID, --file audience.json ({audience:{groupIds|stageIds|personIds|filters}}). send, unschedule and reschedule require --request-key; reuse it after a network error. Templates/series are for automation; campaigns have separate drafts. See PEOPLE.md.",
605
+ "Schedule campaigns through save document.scheduleLocal:{dateTime,timeZone} (local YYYY-MM-DDTHH:mm and IANA zone, converted by the server) or document.scheduledAt (future ISO time with timezone); null means Send now, omission preserves timing. A dated draft never sends until campaign send confirms it. Supply review:{audience,eligible,documentUpdatedAt,scheduledAt} from fresh saved reads/preview. campaign get returns status and scheduledAt; pending plus a future date means Scheduled. Direct send email cannot bypass saved timing. Before queueing, unschedule cancels to draft; reschedule changes only time. Both require documentUpdatedAt from fresh campaign get and a stable --request-key. reschedule accepts scheduleLocal or scheduledAt. See MAIL.md, Campaign scheduling.",
603
606
  "template history <id> pages saved copies with --limit (20 by default, max 100) and --cursor. history-get <id> --history-id reads full content. Both reads follow the plan Rewind window, plus Latest at any age; older copies stay stored. Lists report windowDays, unlimited, canUpgrade and moreBeyondWindow. Use template set with the stable template ID to save edited content as latest; past copies stay unchanged. Read template get before editing. History begins with content retained when history shipped, not lost earlier edits.",
604
607
  "email list is unsent drafts only. Sent one-offs are mail sends list",
605
608
  "send --file is an inline one-off (no saved document)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {