frontpage-host 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -2
- package/MAIL.md +50 -0
- package/PEOPLE.md +134 -10
- package/README.md +5 -3
- package/dist/commands/mail.js +1 -1
- package/dist/commands/people.js +6 -40
- package/dist/completions/generate.js +3 -3
- package/dist/registry.js +5 -7
- package/llms.txt +6 -2
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -127,7 +127,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
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
129
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
130
|
-
- People CRM: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|
|
|
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
|
|
133
133
|
|
|
@@ -255,8 +255,12 @@ frontpage doctor --json
|
|
|
255
255
|
|
|
256
256
|
## People groups, stages, rules, and imports
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
New owner-added and CSV-imported contacts default to allowed for marketing. Explicit unknown/not_allowed choices and saved refusals, unsubscribes and delivery restrictions still apply. Imports keep their accepted policy on retry. `mail people --json` returns a specific `reasonCode` for unavailable recipients.
|
|
259
259
|
|
|
260
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
Manual group selection uses `groups.people` (20 per page, phone-only contacts included, no email-permission filter) and `membership.addMany` (1 to 20 IDs, real per-person outcomes, safe retries). Use these through `frontpage people workspace OPERATION --file payload.json`; existing CLI versions support that route. Conditional groups use the read-only `groups.preview`, with `checked`, `unknown`, cursor/limit and optional `expectedConditions` to detect changes between pages. Do not turn a conditional preview into additions or exceptions. See PEOPLE.md for mode changes, partial results and bounds.
|
|
260
264
|
|
|
261
265
|
## Customer Inbox
|
|
262
266
|
|
|
@@ -277,3 +281,7 @@ Customer Inbox team guidance: administrators may set `guidanceEmail` through the
|
|
|
277
281
|
## Emails workspace workflows
|
|
278
282
|
|
|
279
283
|
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. `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.
|
|
284
|
+
|
|
285
|
+
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.
|
|
286
|
+
|
|
287
|
+
Group details Email and stage email tasks use the existing `frontpage mail save` / `save_mail_editor` completion. `attachment` checks saved group/stage emailVersion, creates paused rules, supports shared-template edits with rule revisions, and recovers identical retries. Read MAIL.md, Group and stage email tasks. Template get includes ruleUsage; deletion protects paused and active rule dependencies. Creating or saving never activates, enrolls or sends.
|
package/MAIL.md
CHANGED
|
@@ -31,6 +31,46 @@ The response reports `complete:true` and the saved records. Keep the exact paylo
|
|
|
31
31
|
|
|
32
32
|
The workspace's Back button returns to the source series and keeps unfinished work. A test sends only to the signed-in account in the workspace, and does not complete a step task. CLI test recipients remain explicit and subject to the existing test restrictions.
|
|
33
33
|
|
|
34
|
+
## Group and stage email tasks
|
|
35
|
+
|
|
36
|
+
Group details has an Email section. It shows ordinary rules whose actual trigger is someone joining that group and whose actions send a template or start a series. It uses the same records as Rules. Saved conditions and extra actions stay visible. Moving a rule's placement does not change where it runs.
|
|
37
|
+
|
|
38
|
+
Use the shared Add email control to select a saved template, create a template, or use a saved series. Stage Manage rules uses the same template task. Save the group first. Creating or attaching saves one **paused** rule with no extra conditions. Nothing activates, enrolls or sends until the owner explicitly activates the rule for future events. The older `attachments.create` operation keeps its documented defaults; the new completion below always creates paused rules.
|
|
39
|
+
|
|
40
|
+
The existing command and agent operation handle the complete save:
|
|
41
|
+
|
|
42
|
+
- CLI: `frontpage mail save --file save.json --site YOUR_SITE --json`
|
|
43
|
+
- Public API: `POST /api/v1/sites/YOUR_SITE/mail/save`
|
|
44
|
+
- Agent: `save_mail_editor`
|
|
45
|
+
|
|
46
|
+
First read `groups.list`. Copy the group's `emailVersion` into `attachment.expectedGroup`. For a stage, also copy its `id` and `emailVersion` into `stageId` and `expectedStage`. These are saved configuration checks; changing membership counts does not invalidate the task. Never guess a version.
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"requestKey": "a-stable-group-template-save",
|
|
51
|
+
"document": {
|
|
52
|
+
"kind": "template",
|
|
53
|
+
"name": "Group welcome",
|
|
54
|
+
"subject": "Welcome, {{first_name}}",
|
|
55
|
+
"preheader": "Thanks for joining us.",
|
|
56
|
+
"blocks": [{"type":"paragraph","text":"Welcome to our group."}],
|
|
57
|
+
"variables": []
|
|
58
|
+
},
|
|
59
|
+
"attachment": {
|
|
60
|
+
"groupId": "GROUP_ID_FROM_GROUPS_LIST",
|
|
61
|
+
"expectedGroup": "EXACT_EMAIL_VERSION_FROM_GROUPS_LIST"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The response returns `complete:true`, `document` and `rule`. Repeating the same requestKey and payload returns the same pair. Changed payloads conflict. A failed transaction leaves neither a new template nor a new rule. Keep the exact request after a lost response and retry it before starting another save.
|
|
67
|
+
|
|
68
|
+
To attach an existing source, omit `document`. Add either `attachment.templateId` or `attachment.seriesId` and its `expectedSourceUpdatedAt` from a fresh template/series get. A series needs at least one saved step. The result includes the source and paused rule. To edit shared content from an existing entry rule, supply the existing document with `expectedUpdatedAt`, plus `attachment.ruleId` and `expectedRevision` from `rules.get`. This saves the content while leaving the rule's conditions, actions, version, enabled state, activation time and history unchanged. Stale group, stage, source or rule checks return a conflict; keep the draft and review the saved records before making a revised request.
|
|
69
|
+
|
|
70
|
+
The workspace uses the existing template editor and returns to the source group or Stages after Save & return. Group, rule, stage, picker and template drafts use private recovery scoped to the signed-in user, site and browser tab. Duplicated tabs copy the recovery snapshot once and then save separately. Cancel uses the shared unsaved-changes confirmation. An unconfirmed completion must be recovered before leaving its task. A self-test sends only test content and does not complete a pending group task.
|
|
71
|
+
|
|
72
|
+
Templates remain shared. Changes affect future uses; already queued messages keep their content snapshots. Template get exposes `ruleUsage`; the UI also shows rule usage beside existing dependencies. Deleting a template used by a live rule, including a paused rule, fails with a conflict naming the rule. Remove the template action or archive the rule first. Series, form and receipt dependencies still apply. Archiving a rule keeps its run history and saved actions. Never activate, enroll or send without the user's instruction.
|
|
73
|
+
|
|
34
74
|
## Campaign recipients
|
|
35
75
|
|
|
36
76
|
Include `audience` with a one_off document in the same `mail save` request. Supported keys are `personIds`, `groupIds`, `stageIds`, `filters`, and `addresses`. Use `audience:null` to clear the saved selection. Addresses normalize to lower case. Saving addresses never creates CRM people or records consent. Recipient-only edits count as changes and save with the campaign.
|
|
@@ -83,3 +123,13 @@ Types: campaign, series, confirmation, template, test, inline. Search matches re
|
|
|
83
123
|
The owner agent uses the same services through save_mail_editor, get_mail_people, get_mail_send, update_mail_series, set_mail_series_steps, set_mail_settings and the existing send/preview/enrollment tools. remove_mail_suppression matches the CLI's existing authorized removal. Discuss and Build register the same Email capabilities. Tool schemas and runtime instructions describe complete saves, Off enrollment, waiting/resume timing, all settings/null resets, history filters and retry handling. The agent never takes site authority from model input.
|
|
84
124
|
|
|
85
125
|
Hosted developer guides and article bodies must be updated through the supported Frontpage CLI, using current base revisions. Never edit a customer site's Git branch or invent a second API version for this cutover.
|
|
126
|
+
|
|
127
|
+
The people directory returns `reasonCode`: `allowed`, `permission_missing`, `not_allowed`, `unsubscribed`, `delivery_restricted`, or `no_active_email`. These explain the shared marketing permission decision; selecting people never changes it. New owner additions and CSV imports default to allowed. Explicit refusals, unsubscribes and delivery restrictions still block sending.
|
|
128
|
+
|
|
129
|
+
## People audience and activity
|
|
130
|
+
|
|
131
|
+
People can copy a saved template into an independent campaign, review an entire group/stage/filter audience for one-time series enrollment, and read paged CRM/email history. Use `people workspace series.preview` with `audience`, then the returned exact enrollment batches. Use `people workspace person.activity` with personId and cursor for history. See [PEOPLE.md](./PEOPLE.md#full-audience-series-review-and-person-activity) for limits, receipts, identity and status rules. Saving a draft never sends or enrolls.
|
|
132
|
+
|
|
133
|
+
## MCP People and email permission
|
|
134
|
+
|
|
135
|
+
MCP source access alone cannot read or change email or contacts. Request separate `crm:read` / `crm:write` scopes and choose the matching People and email consent. Existing source-only grants remain unchanged. The shared tools are `frontpage_mail_read`, `frontpage_mail_write`, `frontpage_people_read`, and `frontpage_people_write`; they reuse current services and enforce the same role, plan, permission, saved-version and receipt checks. See the People guide's MCP contract for operation names, paging and uncertain-write recovery. Send, enrollment and activation require the user's instruction.
|
package/PEOPLE.md
CHANGED
|
@@ -6,7 +6,7 @@ People keeps a business’s contacts, notes, follow-ups, and history together. G
|
|
|
6
6
|
|
|
7
7
|
## Get started
|
|
8
8
|
|
|
9
|
-
Use
|
|
9
|
+
Use `frontpage-host` 0.3.0 or later. Sign in, then choose a site using `--site` on each command or `frontpage use YOUR_SITE`. A site is its subdomain slug, such as `acme`, not a full URL or UUID.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
npm install -g frontpage-host@latest
|
|
@@ -43,12 +43,14 @@ frontpage people unhide PERSON_ID --site YOUR_SITE --json
|
|
|
43
43
|
- **Details:** name, email, phone, mailing address, original source, first seen, and last seen. Address fields include recipient name, line 1, line 2, city, region, postal code, and country.
|
|
44
44
|
- **Notes:** `people note PERSON_ID --body "…"` adds a manual note. `people rm-note PERSON_ID --event NOTE_EVENT_ID` removes an owner’s manual note. Source submissions and payments are not removable notes.
|
|
45
45
|
- **Follow-up:** set an ISO date with `--at`; clear it with `people follow-up PERSON_ID --clear`. The person page highlights overdue dates.
|
|
46
|
-
- **Activity:** `people get`
|
|
46
|
+
- **Activity:** `people get` retains its recent 50 CRM events. Use `person.activity` for paged CRM and email history. The person page shows populated categories with loaded counts. Group and stage pages do not show a separate activity feed.
|
|
47
47
|
- **Hide and restore:** hide is recoverable. It retains identity, history, and membership. It excludes the person from campaign audiences and automated emails. Later source ingest may restore them.
|
|
48
48
|
|
|
49
49
|
### Find and export people
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
People list and detail reads include `eligible` and `reasonCode`. The UI shows **Can email** or **Can't email** from the same policy used by the email picker. Codes are `allowed`, `no_active_email`, `delivery_restricted`, `unsubscribed`, `not_allowed`, and `permission_missing`. Hidden or missing/invalid email addresses use `no_active_email`. These read fields do not grant consent or promise sender readiness, budget, or delivery. API, `people list --json`, `people get --json`, `list_people`, and `get_person` return the same fields. CSV now includes groups and stages instead of the retired global status/tag columns.
|
|
52
|
+
|
|
53
|
+
`people list` and `people export` accept search, group, stage, source, form, follow-up, hidden state, sort and recency. List defaults to 50 contacts, supports up to 200 per page, and returns `nextCursor`. Pass that cursor to `--cursor` to continue. Export writes UTF-8 CSV; it is a contact export, not a complete activity archive.
|
|
52
54
|
|
|
53
55
|
## Groups: organize people
|
|
54
56
|
|
|
@@ -70,6 +72,25 @@ frontpage people groups archive --id GROUP_ID --site YOUR_SITE --json
|
|
|
70
72
|
frontpage people groups restore --id GROUP_ID --site YOUR_SITE --json
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
### Select people from a manual group
|
|
76
|
+
|
|
77
|
+
The group header offers **Add people** for manual groups and **View matching people** for conditional groups. Membership selection includes visible phone-only contacts and people without marketing permission. Current members are marked **Already in this group** and cannot be selected.
|
|
78
|
+
|
|
79
|
+
Use `groups.people` with `groupId`, optional `q`, `source`, `sort` (`newest`, `oldest`, `name`), `page`, `selectedOnly` and `selected` IDs. It returns 20 people per page, a full filtered `total`, the actual `page`, phone/email contact details and `member`. It rejects conditional or archived groups and never records consent.
|
|
80
|
+
|
|
81
|
+
Use `membership.addMany` with `groupId` and 1 to 20 `personIds` per request. The UI can select up to 100 and submits batches of 20. Results include `added`, `alreadyMember`, `skipped`, `failed` and per-person `results` with `personId`, `status` and an optional message. Retry unconfirmed or failed IDs: current members are skipped without resetting their stage or repeating join events. Hidden people are skipped; a group that changed to conditional or archived cannot receive new additions through this operation. Successful additions save immediately and keep the normal entry rules. This does not grant email permission or change other groups.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
frontpage people workspace groups.people --group GROUP_ID --data '{"page":1,"q":"Alex"}' --site YOUR_SITE --json
|
|
85
|
+
frontpage people workspace membership.addMany --file selected-people.json --site YOUR_SITE --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`selected-people.json` contains `{"groupId":"GROUP_ID","personIds":["PERSON_ID"]}`. Keep batches bounded and retain failed/unconfirmed selections for retry. Existing single-person membership and exception operations keep their current behavior.
|
|
89
|
+
|
|
90
|
+
`groups.preview` accepts `limit` (1 to 200, default 200) and `cursor`, and returns `checked`, `unknown`, `matches` and `nextCursor`. A page can have no matches while more people remain to check. Counts describe the people checked so far, not saved membership. Match rows include phone as well as email. Pass `expectedConditions` with a saved group ID to reject a changed condition/mode between pages. Explicit `conditions` previews edits without saving them. Neither preview changes membership or sends emails.
|
|
91
|
+
|
|
92
|
+
When changing to **Match conditions**, membership will update to match the conditions, and some people may join or leave. When changing to **Add people**, current members stay and the group stops updating from conditions. Existing exceptions, saved pauses, stages and history stay saved. The UI shows this effect before Save and does not add a confirmation step.
|
|
93
|
+
|
|
73
94
|
### Keep a group matched to conditions
|
|
74
95
|
|
|
75
96
|
An automatic group uses saved contact facts. The worker checks contacts in rotating batches and updates membership as those facts or time change. Preview first. Save this as `repeat-buyers.json`:
|
|
@@ -111,7 +132,7 @@ frontpage people membership hold --person PERSON_ID --group GROUP_ID --hold fals
|
|
|
111
132
|
|
|
112
133
|
**Preserve existing records.**
|
|
113
134
|
|
|
114
|
-
|
|
135
|
+
Groups and stages are the only membership and progress model. Use group and stage IDs. Global status/tag fields, filters and commands are retired. Existing groups, stages, memberships, person IDs and history remain saved. Reads never initialize or convert records. Authorized creation and ingestion set up new sites directly in the current model.
|
|
115
136
|
|
|
116
137
|
Archive groups instead of deleting their history. Assignments stay saved; related rules pause. Restoring a group does not reactivate those rules. Review and activate them explicitly.
|
|
117
138
|
|
|
@@ -185,6 +206,23 @@ frontpage people rules retry --run RUN_ID --site YOUR_SITE --json
|
|
|
185
206
|
frontpage people rules archive --id RULE_ID --site YOUR_SITE --json
|
|
186
207
|
```
|
|
187
208
|
|
|
209
|
+
### Organize rules under a group or stage
|
|
210
|
+
|
|
211
|
+
The stage table opens **Manage rules** without leaving the group. Create a new paused rule, place an existing rule here, or attach a saved email as a paused entry rule. The Rules tab lists group and stage rules together and can filter by placement. The editor shows the real event and ordered actions. Placement never changes the event that starts a rule.
|
|
212
|
+
|
|
213
|
+
`rules.get`, `rules.list`, and `overview` return an opaque `revision` for each rule. To change only `trigger.owner`, call `rules.place` with `id`, `expectedRevision`, and `owner` (`groupId`, optional `stageId`). Use `owner:null` to restore placement inferred from the actual trigger/action groups. It accepts no execution edits. Read the latest rule again after a conflict or an uncertain response before deciding whether to retry.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
frontpage people rules get --id RULE_ID --site YOUR_SITE --json
|
|
217
|
+
frontpage people workspace rules.place --file placement.json --site YOUR_SITE --json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`placement.json` contains `{"id":"RULE_ID","expectedRevision":"REVISION_FROM_READ","owner":{"groupId":"GROUP_ID","stageId":"STAGE_ID"}}`.
|
|
221
|
+
|
|
222
|
+
Placement validates active same-site references and rejects stale revisions with HTTP 409. It preserves the actual trigger, name, conditions, actions, enabled state, execution version, activation time, errors, existing run receipts and scheduled work. It changes only `trigger.owner` and the edit timestamp. General `rules.update` accepts optional `expectedRevision`. Send it to detect conflicting edits. Use general updates for intentional execution changes, which retain their existing version/activation behavior.
|
|
223
|
+
|
|
224
|
+
New UI rules and email attachments save paused; activate them separately. The generic attachment API still defaults to active when `enabled` is omitted, so integrations that want a review step must send `enabled:false`. The UI removes optional condition authoring but shows existing conditions and saves them unchanged. CLI and agent condition support remains available. Duration controls store exact whole minutes while offering minutes, hours, days and weeks; saved non-round values stay exact.
|
|
225
|
+
|
|
188
226
|
`trigger.owner` only places the rule in the editor. It never limits who can start the event. A form rule can belong to a stage without requiring the form submission to contain a group ID. Use `conditions` to limit it to existing members. Use `trigger.groupId` and `trigger.stageId` for membership events themselves.
|
|
189
227
|
|
|
190
228
|
### When: choose a real event
|
|
@@ -347,7 +385,7 @@ Permission belongs to the email address and site. Groups and stages never grant
|
|
|
347
385
|
| Entry point | New contact | Existing contact |
|
|
348
386
|
| --- | --- | --- |
|
|
349
387
|
| Owner UI, `people add`, public API `POST /people`, agent `add_person` | `allowed` by default, recorded as an owner assertion | Omitted choice preserves the saved decision. Explicit choices can fill unknown permission; add cannot reverse a recorded refusal, mailing-list unsubscribe or suppression. |
|
|
350
|
-
| CSV import |
|
|
388
|
+
| CSV import | Allowed by default; an explicit list-wide choice or row value can select otherwise | List-wide choices leave existing decisions alone. Row values can record permission, but cannot reverse a refusal, unsubscribe or suppression. |
|
|
351
389
|
| Forms, orders, bookings and account events | Unknown unless a supported subscriber signup records agreement | Preserve saved permission; purchases and signup timestamps are not email agreement. |
|
|
352
390
|
| Group, stage, name, phone, note and follow-up changes | No permission change | No permission change. Changing an email does not transfer an old address's grant; a recorded refusal stays in effect. |
|
|
353
391
|
|
|
@@ -409,7 +447,7 @@ For a reviewed cohort, submit at most 25 people in each bounded batch. Each row
|
|
|
409
447
|
}
|
|
410
448
|
```
|
|
411
449
|
|
|
412
|
-
Run the same `person permission --file reviewed-batch.json` command. Read every row in `results`: domain conflicts include `error` and `status`, while successful rows include `emailPermission` and `duplicate`. The batch can partly succeed; receipts make a retry safe. Malformed batch input fails before any permission writes. Infrastructure errors can leave completed receipts, so retry the exact batch rather than inventing new keys. This capability
|
|
450
|
+
Run the same `person permission --file reviewed-batch.json` command. Read every row in `results`: domain conflicts include `error` and `status`, while successful rows include `emailPermission` and `duplicate`. The batch can partly succeed; receipts make a retry safe. Malformed batch input fails before any permission writes. Infrastructure errors can leave completed receipts, so retry the exact batch rather than inventing new keys. This capability uses the same public API and `manage_people` operation.
|
|
413
451
|
|
|
414
452
|
## Safe sync through public customer commands
|
|
415
453
|
|
|
@@ -441,7 +479,7 @@ Alex Rivera,alex@example.com,,
|
|
|
441
479
|
Phone customer,,+12025550199,
|
|
442
480
|
```
|
|
443
481
|
|
|
444
|
-
Map exact header names in `mapping.json`. The permission column is optional. `yes`/`true` records an owner assertion and `no`/`false` records refusal. A supplied row value takes priority over the list-wide choice; duplicate rows keep a negative decision. Set top-level `emailPermission` to `allowed`, `unknown`, or `not_allowed` for new people whose row has no permission value. Omission defaults to
|
|
482
|
+
Map exact header names in `mapping.json`. The permission column is optional. `yes`/`true` records an owner assertion and `no`/`false` records refusal. A supplied row value takes priority over the list-wide choice; duplicate rows keep a negative decision. Set top-level `emailPermission` to `allowed`, `unknown`, or `not_allowed` for new people whose row has no permission value. Omission defaults to allowed for new imports. The accepted import stores its effective policy, so retries keep the original choice. List-wide choices never change existing decisions. Imports preserve saved refusals, unsubscribe and suppression. Changing the list choice requires a new request key.
|
|
445
483
|
|
|
446
484
|
```
|
|
447
485
|
{
|
|
@@ -454,7 +492,7 @@ Map exact header names in `mapping.json`. The permission column is optional. `ye
|
|
|
454
492
|
"groupIds": [
|
|
455
493
|
"GROUP_ID"
|
|
456
494
|
],
|
|
457
|
-
"emailPermission": "
|
|
495
|
+
"emailPermission": "allowed"
|
|
458
496
|
}
|
|
459
497
|
```
|
|
460
498
|
|
|
@@ -488,7 +526,7 @@ The worker saves its cursor and each row’s outcome, so closing the editor does
|
|
|
488
526
|
|
|
489
527
|
## Email all people, a group, a stage, or selected people
|
|
490
528
|
|
|
491
|
-
The People button names the selected audience, such as **Email all people** or **Email Newsletter**. It opens a one-off draft in Emails. Drafting and sending stay there.
|
|
529
|
+
The People button names the selected audience, such as **Email all people** or **Email Newsletter**. It opens a one-off draft in Emails. Drafting and sending stay there. Use a template creates an independent campaign draft; editing it never changes its source template.
|
|
492
530
|
|
|
493
531
|
Save `audience.json`:
|
|
494
532
|
|
|
@@ -582,7 +620,7 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
582
620
|
|
|
583
621
|
| Operation | Payload fields | Purpose |
|
|
584
622
|
| ------------------------------------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
585
|
-
| overview | — | Active groups, stages, rules, saved emails
|
|
623
|
+
| overview | — | Active groups, stages, rules, saved emails and series. Reads never initialize records. |
|
|
586
624
|
| sources | — | Supported events and current form/product IDs. |
|
|
587
625
|
| groups.list | archived? | List groups; archived:true includes archived groups. |
|
|
588
626
|
| groups.get | id | One group with its stages. |
|
|
@@ -590,11 +628,13 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
590
628
|
| groups.update | id, changed fields | Edit a group without changing its ID. |
|
|
591
629
|
| groups.archive / groups.restore | id | Archive or restore a group. Related rules remain paused on restore. |
|
|
592
630
|
| groups.preview | id or conditions; personId? or cursor? | Check conditions without changing membership. |
|
|
631
|
+
| groups.people | groupId; q?, source?, sort?, page?, selectedOnly?, selected? | Read the manual-group picker, including phone-only contacts and current membership. |
|
|
593
632
|
| stages.create | groupId, label, color?, position?, terminal? | Create a stage in a saved group. |
|
|
594
633
|
| stages.update | id, groupId, changed fields | Edit a stage. |
|
|
595
634
|
| stages.reorder | groupId, stageIds | Save every active stage ID in order, once each. |
|
|
596
635
|
| stages.archive / stages.restore | id, groupId | Keep assignments and history while archiving or restoring. |
|
|
597
636
|
| membership.get | personId | Memberships and email eligibility. |
|
|
637
|
+
| membership.addMany | groupId, personIds (1 to 20) | Add visible people to a manual group, reporting real per-person outcomes; retries preserve current members. |
|
|
598
638
|
| membership.add / membership.remove | personId, groupId; reset? on add | Join or leave a group; a conditions group stores an include or exclude exception. |
|
|
599
639
|
| membership.move | personId, fromGroupId, groupId, reset? | Leave one manual group and join another. |
|
|
600
640
|
| membership.stage | personId, groupId, stageId | Set a stage; null clears it without leaving the group. |
|
|
@@ -605,6 +645,7 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
605
645
|
| rules.get | id | Read the saved rule and version. |
|
|
606
646
|
| rules.create | name, trigger, conditions?, actions, enabled? | Create a rule. Pass enabled:false to review it first. |
|
|
607
647
|
| rules.update | id, changed fields | Save a new rule version. |
|
|
648
|
+
| rules.place | id, owner, expectedRevision | Change display placement only; preserve execution and reject stale saves. |
|
|
608
649
|
| rules.enable / rules.pause / rules.archive | id | Activate future events, pause, or archive. |
|
|
609
650
|
| rules.validate | rule fields or id | Validate references and automation cycles without saving. |
|
|
610
651
|
| rules.preview | rule fields or id; personId? | Evaluate a person or return validation only; sends nothing. |
|
|
@@ -648,3 +689,86 @@ To enrol, copy each returned batch's `personIds` and `reviewToken` into `series.
|
|
|
648
689
|
Manual series enrollment uses selected person IDs and saved steps. Off records real enrollment waiting at step 1, without a send, snapshot, timer or allowance reservation; no second Save series is needed. On starts the first email subject to sender, permission, send window and allowance checks. Turning On resumes existing people at their next unsent step with its due date intact; later waits start after successful sends. Review returns status, exclusions and nullable remainingAllowance (null while Off). It never activates a series or restarts existing, completed or cancelled enrollment, including a person whose email changed. Delivery rechecks saved restrictions. Contact groups and permission stay unchanged. Campaign recipient intent also supports explicit addresses and null to clear; saving never sends or grants consent.
|
|
649
690
|
|
|
650
691
|
People supplies first-name and unsubscribe values. If content uses order, booking or other caller-only variables, edit the independent campaign or choose another series. Do not invent values or send empty required context.
|
|
692
|
+
|
|
693
|
+
## Group and stage email tasks
|
|
694
|
+
|
|
695
|
+
Group details has an Email section. It shows ordinary rules whose actual trigger is someone joining that group and whose actions send a template or start a series. It uses the same records as Rules. Saved conditions and extra actions stay visible. Moving a rule's placement does not change where it runs.
|
|
696
|
+
|
|
697
|
+
Use the shared Add email control to select a saved template, create a template, or use a saved series. Stage Manage rules uses the same template task. Save the group first. Creating or attaching saves one **paused** rule with no extra conditions. Nothing activates, enrolls or sends until the owner explicitly activates the rule for future events. The older `attachments.create` operation keeps its documented defaults; the new completion below always creates paused rules.
|
|
698
|
+
|
|
699
|
+
The existing command and agent operation handle the complete save:
|
|
700
|
+
|
|
701
|
+
- CLI: `frontpage mail save --file save.json --site YOUR_SITE --json`
|
|
702
|
+
- Public API: `POST /api/v1/sites/YOUR_SITE/mail/save`
|
|
703
|
+
- Agent: `save_mail_editor`
|
|
704
|
+
|
|
705
|
+
First read `groups.list`. Copy the group's `emailVersion` into `attachment.expectedGroup`. For a stage, also copy its `id` and `emailVersion` into `stageId` and `expectedStage`. These are saved configuration checks; changing membership counts does not invalidate the task. Never guess a version.
|
|
706
|
+
|
|
707
|
+
```json
|
|
708
|
+
{
|
|
709
|
+
"requestKey": "a-stable-group-template-save",
|
|
710
|
+
"document": {
|
|
711
|
+
"kind": "template",
|
|
712
|
+
"name": "Group welcome",
|
|
713
|
+
"subject": "Welcome, {{first_name}}",
|
|
714
|
+
"preheader": "Thanks for joining us.",
|
|
715
|
+
"blocks": [{"type":"paragraph","text":"Welcome to our group."}],
|
|
716
|
+
"variables": []
|
|
717
|
+
},
|
|
718
|
+
"attachment": {
|
|
719
|
+
"groupId": "GROUP_ID_FROM_GROUPS_LIST",
|
|
720
|
+
"expectedGroup": "EXACT_EMAIL_VERSION_FROM_GROUPS_LIST"
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
The response returns `complete:true`, `document` and `rule`. Repeating the same requestKey and payload returns the same pair. Changed payloads conflict. A failed transaction leaves neither a new template nor a new rule. Keep the exact request after a lost response and retry it before starting another save.
|
|
726
|
+
|
|
727
|
+
To attach an existing source, omit `document`. Add either `attachment.templateId` or `attachment.seriesId` and its `expectedSourceUpdatedAt` from a fresh template/series get. A series needs at least one saved step. The result includes the source and paused rule. To edit shared content from an existing entry rule, supply the existing document with `expectedUpdatedAt`, plus `attachment.ruleId` and `expectedRevision` from `rules.get`. This saves the content while leaving the rule's conditions, actions, version, enabled state, activation time and history unchanged. Stale group, stage, source or rule checks return a conflict; keep the draft and review the saved records before making a revised request.
|
|
728
|
+
|
|
729
|
+
The workspace uses the existing template editor and returns to the source group or Stages after Save & return. Group, rule, stage, picker and template drafts use private recovery scoped to the signed-in user, site and browser tab. Duplicated tabs copy the recovery snapshot once and then save separately. Cancel uses the shared unsaved-changes confirmation. An unconfirmed completion must be recovered before leaving its task. A self-test sends only test content and does not complete a pending group task.
|
|
730
|
+
|
|
731
|
+
Templates remain shared. Changes affect future uses; already queued messages keep their content snapshots. Template get exposes `ruleUsage`; the UI also shows rule usage beside existing dependencies. Deleting a template used by a live rule, including a paused rule, fails with a conflict naming the rule. Remove the template action or archive the rule first. Series, form and receipt dependencies still apply. Archiving a rule keeps its run history and saved actions. Never activate, enroll or send without the user's instruction.
|
|
732
|
+
|
|
733
|
+
## Full audience series review and person activity
|
|
734
|
+
|
|
735
|
+
`series.preview` accepts either the existing `personIds` array (up to 1,000 IDs) or an `audience` object, never both. Audience uses the existing People matcher: `personIds`, `groupIds`, `stageIds`, and supported `filters`. ID lists combine as a union; filters narrow the result. All people uses `{"filters":{}}`. Series audiences cannot contain `addresses`. Group/stage IDs must belong to this site and remain available.
|
|
736
|
+
|
|
737
|
+
The service resolves the whole audience at explicit review, up to 10,000 people, without using the list's page or sort. It returns the existing counts, eligiblePeople, excluded reasons and reviewed batches. An empty matched audience returns zero counts and no batches. The UI keeps the eligible total and omits exclusion details; CLI/agent responses retain reasons.
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
frontpage people workspace series.preview --site YOUR_SITE --data '{"seriesId":"SERIES_ID","audience":{"groupIds":["GROUP_ID"],"filters":{"q":"Alex"}}}' --json
|
|
741
|
+
frontpage people workspace person.activity --site YOUR_SITE --data '{"personId":"PERSON_ID","limit":50}' --json
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Copy each returned batch's exact `personIds` and `reviewToken` into `series.enrol`, with the series ID and a unique stable requestKey for that batch. Keep completed batches and retry uncertain batches with the original payload and key. Reusing a key with other IDs conflicts. Existing and completed/cancelled enrollments never restart. Off waits at step 1 without sending; On uses the existing sender, permission, window and allowance checks. This is a one-time selection captured at review: later membership changes do not add or cancel manual enrollments. Use an explicit group-entry rule for later entrants. Never enroll or send without the user's instruction.
|
|
745
|
+
|
|
746
|
+
`person.activity` is a read-only People workspace operation shared by UI, CLI, public API and agent. It returns `events`, `total`, and `nextCursor`; limit defaults to 50 and accepts 1–100. Pass the same personId and returned cursor to load older entries. Cursors keep timestamp precision and tie-break by stable entry identity; a cursor from another person/site is invalid. Queue time fixes email ordering, so delivery updates do not move it into another page. Totals are current reads and can change as activity arrives. UI tab counts refer to loaded entries.
|
|
747
|
+
|
|
748
|
+
The feed combines CRM history and mail ledger rows. Mail entries use `id: "mail:SEND_ID"`, `type: "email.send"`, and payload containing sendId, subject, recipient snapshot, kind, test, status, error and status update time. Read `frontpage mail sends get --id SEND_ID` for saved content and source details. Workspace links open that same saved send even when the source was deleted. `sent` means provider accepted; only `delivered` means a saved delivery confirmation. Drafts and waiting enrollments are not sends.
|
|
749
|
+
|
|
750
|
+
A saved send snapshot's crmPersonId supplies identity. Missing identity remains unbound; the read never guesses from today's email address. New series enrollments also save their person identity before any email is queued. It shows an explicitly linked CRM email event only once through the ledger. New campaign recipient snapshots retain the approved person ID, and queueing skips a recipient whose address moved to another person. Existing queued content, permission checks, group-removal rules and suppression behavior stay in place.
|
|
751
|
+
|
|
752
|
+
## Groups and stages cutover
|
|
753
|
+
|
|
754
|
+
Groups and optional stages are the only membership/progress model. The global status/tag API routes, tool names and CLI flags are retired. Use `--group`/`--stage`, `membership.add`/`remove` and `membership.stage`. Exports include group and stage labels; they omit global status and tags. Saved historical events still display their original facts.
|
|
755
|
+
|
|
756
|
+
Opening overview, list, summary, activity or export does not initialize records. Authorized first creation or ingestion sets up a new site's normal progress group and stages. Existing default group IDs, assignments, permissions, paused work and rule receipts remain saved.
|
|
757
|
+
|
|
758
|
+
### MCP access requires separate consent
|
|
759
|
+
|
|
760
|
+
Source-file access does not grant contact access. Request `sites:read crm:read` for People/email reads, or `sites:read crm:read crm:write` for management. Choose People and email access on the consent screen. It defaults to **No People or email access**. Existing connections do not gain these scopes. Management can send email or activate automation on the user's instruction; site role, plan, permission and delivery checks still apply.
|
|
761
|
+
|
|
762
|
+
- `frontpage_get_crm_docs`: read the current `people` or `mail` operation guide with bounded text paging. It shares no customer data.
|
|
763
|
+
- `frontpage_people_read`: every shared read operation, plus `contacts.list`, `contacts.get`, `contacts.summary`, `contacts.export`.
|
|
764
|
+
- `frontpage_people_write`: every shared write operation, plus `contacts.add`, `contacts.update`, `contacts.hide`, `contacts.unhide`, `contacts.followUp`, `contacts.notes.add`, `contacts.notes.remove`.
|
|
765
|
+
- `frontpage_mail_read`: existing read/preview Email tool names, `get_mail_settings`, `campaign.get`, `campaign.preview`.
|
|
766
|
+
- `frontpage_mail_write`: existing Email write tool names, `campaign.audience`, `campaign.send`.
|
|
767
|
+
|
|
768
|
+
The four People/Mail data tools take `slug`, `operation`, and `payload`. The docs tool takes `topic`, `offset`, and `length`. The verified connection supplies site authority and actor; payload cannot override slug, siteId, actor, operation or area. Contact operations use `personId`; note removal also uses `eventId`. Follow-up uses `at` (ISO timestamp or null). Email tool names and payloads match the Mail guide and existing website-agent tools. `save_mail_editor` uses the shared receipt-backed completion. A campaign call omits the `campaign.` prefix inside its payload because operation already selects it.
|
|
769
|
+
|
|
770
|
+
Read tools accept `resultOffset` (default 0) and `resultLimit` (default 50, maximum 100) to page top-level result arrays. Continue with `resultPage.nextOffset` and the same operation/payload. `resultPage.totals` records each array's full length. Native list/activity cursors belong in payload. Audience review pages recompute the read; always retain each exact returned batch and review token, and re-review after a stale-token conflict. Large records may require a smaller native limit; no result is silently truncated.
|
|
771
|
+
|
|
772
|
+
Writes are not universally idempotent. Preserve documented request keys, receipts, revisions and review tokens. After an unknown outcome, read back before creating again; retry receipt-backed operations with their exact original key and payload. Never send, enroll or activate without the user's instruction.
|
|
773
|
+
|
|
774
|
+
For MCP CSV exports, continue `contacts.export` with `textOffset` set to `textPage.nextOffset` and `expectedTextVersion` set to `textPage.version`. The tool returns bounded `csv` text and a version hash; a changed export returns a conflict instead of combining different snapshots. `textLength` defaults to 4,000 UTF-16 units and accepts up to 8,000.
|
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Frontpage CLI — manage your site from the terminal.
|
|
4
4
|
|
|
5
|
-
**npm package:** `frontpage-host`
|
|
6
|
-
**Binary:** `frontpage`
|
|
5
|
+
**npm package:** `frontpage-host`
|
|
6
|
+
**Binary:** `frontpage`
|
|
7
7
|
**Do not install** the unrelated npm package named `frontpage`.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
@@ -59,7 +59,7 @@ See [AGENTS.md](./AGENTS.md) and [llms.txt](./llms.txt) in this package.
|
|
|
59
59
|
|
|
60
60
|
## People groups, stages, rules, and imports
|
|
61
61
|
|
|
62
|
-
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import
|
|
62
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
Payments, invoices and recurring billing: see packaged [PAYMENTS.md](./PAYMENTS.md), `frontpage help payments`, and https://frontpage.host/developers/payments. Version 0.2.0 removes writable currency, Store checkout mode and Store-owned money commands; Store stays cart-only. Preview/review before sends; keep stable operation keys on retries. Autopay requires the customer’s own verified authorization.
|
|
@@ -94,3 +94,5 @@ A whole stock count of `0` means sold out; `unlimited` sends JSON null. Omit sto
|
|
|
94
94
|
## Emails workspace workflows
|
|
95
95
|
|
|
96
96
|
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. `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.
|
|
97
|
+
|
|
98
|
+
Group details Email and stage email tasks use the existing `frontpage mail save` / `save_mail_editor` completion. `attachment` checks saved group/stage emailVersion, creates paused rules, supports shared-template edits with rule revisions, and recovers identical retries. Read MAIL.md, Group and stage email tasks. Template get includes ruleUsage; deletion protects paused and active rule dependencies. Creating or saving never activates, enrolls or sends.
|
package/dist/commands/mail.js
CHANGED
|
@@ -123,7 +123,7 @@ export async function runMail(args) {
|
|
|
123
123
|
emitOk("mail", data);
|
|
124
124
|
else
|
|
125
125
|
for (const p of (data.people ?? []))
|
|
126
|
-
console.log(`${p.id} ${p.email ?? ""} ${p.eligible ? "eligible" : "restricted"}`);
|
|
126
|
+
console.log(`${p.id} ${p.email ?? ""} ${p.eligible ? "eligible" : (p.reasonCode ?? "restricted").replaceAll("_", " ")}`);
|
|
127
127
|
return EXIT_OK;
|
|
128
128
|
}
|
|
129
129
|
if (group === "settings") {
|
package/dist/commands/people.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveSlug } from "../site.js";
|
|
|
4
4
|
import { CliHttpError, EXIT_OK, EXIT_USAGE, apiBase, exitCodeForError, getBearerAuthHeader, httpErrorFromV1, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
|
|
5
5
|
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
6
6
|
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
7
|
-
const USAGE = "Usage: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|
|
|
7
|
+
const USAGE = "Usage: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|export [--site <slug>] [--json]";
|
|
8
8
|
const PERSON_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
9
|
function requireId(work) {
|
|
10
10
|
const fromFlag = takeFlag(work, "--id");
|
|
@@ -33,7 +33,7 @@ function displayName(row) {
|
|
|
33
33
|
return row.name || row.email || row.phone || row.id;
|
|
34
34
|
}
|
|
35
35
|
function printPerson(row) {
|
|
36
|
-
const bits = [
|
|
36
|
+
const bits = [displayName(row)];
|
|
37
37
|
if (row.email && row.email !== row.name)
|
|
38
38
|
bits.push(row.email);
|
|
39
39
|
if (row.phone && row.phone !== row.name)
|
|
@@ -47,8 +47,6 @@ function printPerson(row) {
|
|
|
47
47
|
bits.push(`follow-up ${row.followUpAt}`);
|
|
48
48
|
if (row.hiddenAt)
|
|
49
49
|
bits.push("hidden");
|
|
50
|
-
if (row.tags?.length)
|
|
51
|
-
bits.push(row.tags.join(","));
|
|
52
50
|
if (row.lastEvent?.summary)
|
|
53
51
|
bits.push(row.lastEvent.summary);
|
|
54
52
|
console.log(`${row.id} ${bits.join(" · ")}`);
|
|
@@ -74,13 +72,14 @@ export async function runPeople(args) {
|
|
|
74
72
|
try {
|
|
75
73
|
const { slug, rest } = await resolveSlug(args);
|
|
76
74
|
const sub = rest[0];
|
|
75
|
+
if (sub === "tag" || args.some(arg => ["--status", "--tag"].includes(arg.split("=", 1)[0])))
|
|
76
|
+
throw new CliHttpError("Global status and tags are retired. Use people groups, membership and stages; filter with --group and --stage.", EXIT_USAGE, 400, "bad_request");
|
|
77
77
|
if (isPeopleWorkspaceCommand(sub))
|
|
78
78
|
return await runPeopleWorkspace(slug, rest);
|
|
79
79
|
const base = `/api/v1/sites/${encodeURIComponent(slug)}/people`;
|
|
80
80
|
if (!sub || sub === "list") {
|
|
81
81
|
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
82
82
|
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
83
|
-
const status = takeFlag(work, "--status");
|
|
84
83
|
const groupId = takeFlag(work, "--group");
|
|
85
84
|
const stageId = takeFlag(work, "--stage");
|
|
86
85
|
const source = takeFlag(work, "--source");
|
|
@@ -91,7 +90,6 @@ export async function runPeople(args) {
|
|
|
91
90
|
const limit = takeFlag(work, "--limit");
|
|
92
91
|
const cursor = takeFlag(work, "--cursor");
|
|
93
92
|
const hidden = hasFlag(work, "--hidden");
|
|
94
|
-
const tag = takeFlag(work, "--tag");
|
|
95
93
|
assertNoUnknownFlags(work, "people");
|
|
96
94
|
const params = new URLSearchParams();
|
|
97
95
|
if (q)
|
|
@@ -100,8 +98,6 @@ export async function runPeople(args) {
|
|
|
100
98
|
params.set("groupId", groupId);
|
|
101
99
|
if (stageId)
|
|
102
100
|
params.set("stageId", stageId);
|
|
103
|
-
if (status)
|
|
104
|
-
params.set("status", status);
|
|
105
101
|
if (source)
|
|
106
102
|
params.set("source", source);
|
|
107
103
|
if (formSlug)
|
|
@@ -118,8 +114,6 @@ export async function runPeople(args) {
|
|
|
118
114
|
params.set("cursor", cursor);
|
|
119
115
|
if (hidden)
|
|
120
116
|
params.set("hidden", "1");
|
|
121
|
-
if (tag)
|
|
122
|
-
params.set("tag", tag);
|
|
123
117
|
const qs = params.toString();
|
|
124
118
|
const data = await v1Get(qs ? `${base}?${qs}` : base);
|
|
125
119
|
if (isJson()) {
|
|
@@ -187,7 +181,7 @@ export async function runPeople(args) {
|
|
|
187
181
|
const work = rest.slice(1);
|
|
188
182
|
const id = requireId(work);
|
|
189
183
|
const body = {};
|
|
190
|
-
for (const field of ["name", "email", "phone", "notes"
|
|
184
|
+
for (const field of ["name", "email", "phone", "notes"]) {
|
|
191
185
|
const v = takeFlag(work, `--${field}`);
|
|
192
186
|
if (v !== undefined)
|
|
193
187
|
body[field] = v === "" ? null : v;
|
|
@@ -217,7 +211,7 @@ export async function runPeople(args) {
|
|
|
217
211
|
}
|
|
218
212
|
assertNoUnknownFlags(work, "people");
|
|
219
213
|
if (Object.keys(body).length === 0) {
|
|
220
|
-
console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--
|
|
214
|
+
console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--line1 … --city … --region … --postal … --country …] [--clear-address]");
|
|
221
215
|
return EXIT_USAGE;
|
|
222
216
|
}
|
|
223
217
|
const data = await v1Patch(`${base}/${encodeURIComponent(id)}`, body);
|
|
@@ -304,32 +298,9 @@ export async function runPeople(args) {
|
|
|
304
298
|
console.log(`Restored ${displayName(data.person)}`);
|
|
305
299
|
return EXIT_OK;
|
|
306
300
|
}
|
|
307
|
-
if (sub === "tag") {
|
|
308
|
-
const work = rest.slice(1);
|
|
309
|
-
const id = requireId(work);
|
|
310
|
-
const add = takeFlag(work, "--add");
|
|
311
|
-
const remove = takeFlag(work, "--remove") ?? takeFlag(work, "--rm");
|
|
312
|
-
assertNoUnknownFlags(work, "people");
|
|
313
|
-
if ((!add && !remove) || (add && remove)) {
|
|
314
|
-
console.error("Usage: frontpage people tag <id> --add <tag> | --remove <tag>");
|
|
315
|
-
return EXIT_USAGE;
|
|
316
|
-
}
|
|
317
|
-
const data = add
|
|
318
|
-
? await v1Post(`${base}/${encodeURIComponent(id)}/tags`, {
|
|
319
|
-
tag: add,
|
|
320
|
-
})
|
|
321
|
-
: await v1Delete(`${base}/${encodeURIComponent(id)}/tags/${encodeURIComponent(remove)}`);
|
|
322
|
-
if (isJson()) {
|
|
323
|
-
emitOk("people", data);
|
|
324
|
-
return EXIT_OK;
|
|
325
|
-
}
|
|
326
|
-
printPerson(data.person);
|
|
327
|
-
return EXIT_OK;
|
|
328
|
-
}
|
|
329
301
|
if (sub === "export") {
|
|
330
302
|
const work = rest.slice(1);
|
|
331
303
|
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
332
|
-
const status = takeFlag(work, "--status");
|
|
333
304
|
const groupId = takeFlag(work, "--group");
|
|
334
305
|
const stageId = takeFlag(work, "--stage");
|
|
335
306
|
const source = takeFlag(work, "--source");
|
|
@@ -337,7 +308,6 @@ export async function runPeople(args) {
|
|
|
337
308
|
const followUp = takeFlag(work, "--follow-up");
|
|
338
309
|
const sort = takeFlag(work, "--sort");
|
|
339
310
|
const lastSeenDays = takeFlag(work, "--last-seen-days");
|
|
340
|
-
const tag = takeFlag(work, "--tag");
|
|
341
311
|
const out = takeFlag(work, "--out");
|
|
342
312
|
const hidden = hasFlag(work, "--hidden");
|
|
343
313
|
assertNoUnknownFlags(work, "people");
|
|
@@ -348,8 +318,6 @@ export async function runPeople(args) {
|
|
|
348
318
|
params.set("groupId", groupId);
|
|
349
319
|
if (stageId)
|
|
350
320
|
params.set("stageId", stageId);
|
|
351
|
-
if (status)
|
|
352
|
-
params.set("status", status);
|
|
353
321
|
if (source)
|
|
354
322
|
params.set("source", source);
|
|
355
323
|
if (formSlug)
|
|
@@ -362,8 +330,6 @@ export async function runPeople(args) {
|
|
|
362
330
|
params.set("lastSeenDays", lastSeenDays);
|
|
363
331
|
if (hidden)
|
|
364
332
|
params.set("hidden", "1");
|
|
365
|
-
if (tag)
|
|
366
|
-
params.set("tag", tag);
|
|
367
333
|
const qs = params.toString();
|
|
368
334
|
const csv = await fetchPeopleCsv(qs ? `${base}/export?${qs}` : `${base}/export`);
|
|
369
335
|
if (out) {
|
|
@@ -25,7 +25,7 @@ _frontpage_completions() {
|
|
|
25
25
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
26
26
|
articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
|
|
27
27
|
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
28
|
-
people) COMPREPLY=( $(compgen -W "list get add set note rm-note follow-up hide unhide" -- "\${cur}") ) ;;
|
|
28
|
+
people) COMPREPLY=( $(compgen -W "overview sources groups stages membership rules attachments import person audience workspace list get add set note rm-note follow-up hide unhide export" -- "\${cur}") ) ;;
|
|
29
29
|
routines) COMPREPLY=( $(compgen -W "list get add set pause resume rm run runs" -- "\${cur}") ) ;;
|
|
30
30
|
buttons) COMPREPLY=( $(compgen -W "list get create update archive restore" -- "\${cur}") ) ;;
|
|
31
31
|
mail) COMPREPLY=( $(compgen -W "save people campaign settings email template series send sends enrolments suppress --file --q --status --type --page --limit --cursor --source --sort --availability --selected --selected-only --offset --header-align --window-start --window-end --hourly-cap --site --json" -- "\${cur}") ) ;;
|
|
@@ -73,7 +73,7 @@ _frontpage() {
|
|
|
73
73
|
tokens) _values 'subcommand' set ;;
|
|
74
74
|
seo|settings) _values 'subcommand' get set ;;
|
|
75
75
|
analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
|
|
76
|
-
people) _values 'subcommand' list get add set note rm-note follow-up hide unhide ;;
|
|
76
|
+
people) _values 'subcommand' overview sources groups stages membership rules attachments import person audience workspace list get add set note rm-note follow-up hide unhide export ;;
|
|
77
77
|
routines) _values 'subcommand' list get add set pause resume rm run runs ;;
|
|
78
78
|
mail) _values 'subcommand' save people campaign settings email template series send sends enrolments suppress ;;
|
|
79
79
|
buttons) _values 'subcommand' list get create update archive restore ;;
|
|
@@ -114,7 +114,7 @@ export function generateFishCompletion() {
|
|
|
114
114
|
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
115
115
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
116
116
|
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
117
|
-
`complete -c frontpage -n "__fish_seen_subcommand_from people" -a "list get add set note rm-note follow-up hide unhide"`,
|
|
117
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from people" -a "overview sources groups stages membership rules attachments import person audience workspace list get add set note rm-note follow-up hide unhide export"`,
|
|
118
118
|
`complete -c frontpage -n "__fish_seen_subcommand_from routines" -a "list get add set pause resume rm run runs"`,
|
|
119
119
|
`complete -c frontpage -n "__fish_seen_subcommand_from mail" -a "save people campaign settings email template series send sends enrolments suppress"`,
|
|
120
120
|
`complete -c frontpage -n "__fish_seen_subcommand_from buttons" -a "list get create update archive restore"`,
|
package/dist/registry.js
CHANGED
|
@@ -468,7 +468,7 @@ export const COMMANDS = [
|
|
|
468
468
|
{
|
|
469
469
|
name: "people",
|
|
470
470
|
summary: "People, groups, stages, rules, imports, and campaign audiences",
|
|
471
|
-
usage: "frontpage people overview|sources|groups|stages|membership|rules|attachments|import|
|
|
471
|
+
usage: "frontpage people overview|sources|groups|stages|membership|rules|attachments|import|person|audience|workspace … [--site <slug>] [--json]; list|get|add|set|note|rm-note|follow-up|hide|unhide|export remain available",
|
|
472
472
|
topics: ["content"],
|
|
473
473
|
examples: [
|
|
474
474
|
"frontpage people overview --json",
|
|
@@ -479,21 +479,19 @@ export const COMMANDS = [
|
|
|
479
479
|
"frontpage people import start --csv customers.csv --file mapping.json --request-key REQUEST_UUID",
|
|
480
480
|
"frontpage people list",
|
|
481
481
|
"frontpage people add --email alex@example.com --name Alex",
|
|
482
|
-
"frontpage people
|
|
482
|
+
"frontpage people membership stage --person PERSON_ID --group GROUP_ID --stage STAGE_ID",
|
|
483
483
|
'frontpage people set <id> --line1 "12 Main" --city Austin --region TX --postal 78701 --country US',
|
|
484
484
|
'frontpage people note <id> --body "Called them"',
|
|
485
485
|
"frontpage people follow-up <id> --at 2026-09-12T00:00:00Z",
|
|
486
486
|
"frontpage people hide <id>",
|
|
487
|
-
"frontpage people tag <id> --add vip",
|
|
488
|
-
"frontpage people list --tag vip",
|
|
489
487
|
"frontpage people export",
|
|
490
488
|
],
|
|
491
489
|
flags: [
|
|
492
490
|
"Workspace: groups list|get|create|update|archive|restore|preview; stages create|update|reorder|archive|restore; membership get|add|remove|move|stage|advance|override|hold; rules list|get|create|update|enable|pause|archive|validate|preview|runs|retry; attachments create; import preview|start|get|retry; person permission; audience preview; overview; sources",
|
|
493
491
|
"All workspace operations accept --file JSON or --data JSON. Named flags: --id --label --name --group --stage --person --from-group --mode --color --position --terminal --enabled --hold --allowed --archived --reset --override include|exclude|auto --email-kind template|series --email-id --rule --run --cursor --limit --request-key. Boolean flags take true|false. Complex conditions/actions/mappings use JSON.",
|
|
494
|
-
"CSV: --csv PATH plus --file mapping.json. import start requires a stable --request-key. Retry the same import with the same key and contents.",
|
|
492
|
+
"CSV: --csv PATH plus --file mapping.json. New contacts default to allowed; JSON emailPermission can select unknown or not_allowed. Row values override the list choice; saved refusals, unsubscribes and suppressions remain protected. import start requires a stable --request-key. Retry the same import with the same key and contents.",
|
|
495
493
|
"list/export: --group GROUP_ID --stage STAGE_ID. See package PEOPLE.md for full examples and every JSON shape.",
|
|
496
|
-
"list: --q --
|
|
494
|
+
"list: --q --group --stage --source --form-slug --follow-up due|upcoming|none --hidden --sort --last-seen-days --limit --cursor",
|
|
497
495
|
"add requires --email and/or --phone (never name alone). New contacts default to owner-asserted email permission; --email-permission allowed|unknown|not_allowed changes this choice. Existing default decisions stay saved; withdrawals/restrictions cannot be overridden by add.",
|
|
498
496
|
"tag: --add <label> | --remove <slug>",
|
|
499
497
|
"export writes UTF-8 CSV (same filters as list). Optional --out <file>",
|
|
@@ -833,7 +831,7 @@ export const TOPICS = {
|
|
|
833
831
|
frontpage media list|upload <file> # images/PDF/video; text/CSV = inline not library; sitePath first when stored
|
|
834
832
|
frontpage images generate --prompt "…" [--count N] # 1–4 AI variations of one prompt; sitePath first; AI $ + storage
|
|
835
833
|
frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv]
|
|
836
|
-
frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|
|
|
834
|
+
frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|export
|
|
837
835
|
# People CRM guide: https://frontpage.host/developers/people
|
|
838
836
|
frontpage routines list|get|add|set|pause|resume|rm|run|runs
|
|
839
837
|
# Routines guide: https://frontpage.host/developers/routines
|
package/llms.txt
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- Version: frontpage --version | -V | version
|
|
17
17
|
- SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
|
|
18
18
|
- Redirects: frontpage redirects list|set|rm — HTTP 301/308, instant, not a file; revert does not undo; do not fake with Astro.redirect / meta refresh
|
|
19
|
-
- People CRM: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|
|
|
19
|
+
- 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. Unknown leftover flags fail closed. Guide: https://frontpage.host/developers/people
|
|
20
20
|
- Routines: frontpage routines list|get|add|set|pause|resume|rm|run|runs. Several hours and several days are one routine. 8am,12pm,4pm and tue,thu,fri store hours [8, 12, 16] and weekdays [2, 4, 5]. Timezone is IANA. Do not guess UTC. Guide: https://frontpage.host/developers/routines
|
|
21
21
|
- 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 /__media/uploads/<this-slug>/… or an HTTPS URL already saved on this site. frontpage payments connect [--no-open] opens Frontpage then Stripe. Store is cart-only; currency comes from Stripe and cannot be set. Use frontpage 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". Leave data-fp-store-variants empty. Do not bake titles or prices. Guide: https://frontpage.host/developers/store
|
|
22
22
|
- Analytics funnel: --steps JSON array or comma paths (/, /pricing); bad JSON → Invalid JSON for --steps
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
|
|
35
35
|
## People groups, stages, rules, and imports
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
New owner-added and CSV-imported contacts default to allowed for marketing. Explicit unknown/not_allowed choices and saved refusals, unsubscribes and delivery restrictions still apply. Imports keep their accepted policy on retry. `mail people --json` returns a specific `reasonCode` for unavailable recipients.
|
|
38
|
+
|
|
39
|
+
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`. Use `frontpage mail campaign` for one-off group/stage/list audiences in Emails. Every editor action is available through these commands or `frontpage people workspace OPERATION --file payload.json`. Full operations and examples: [PEOPLE.md](./PEOPLE.md); public guide: https://frontpage.host/developers/people. Automated emails select a saved template or series. Campaigns use their own drafts.
|
|
38
40
|
|
|
39
41
|
Customer Inbox (Scale and internal sites): CUSTOMER-INBOX.md. Settings, widget, knowledge, human guidance/relay, lifecycle, export/download and recovery. The owner toolbar manages all site-wide Inbox operations through manage_customer_inbox, including conversations, files and recovery, but cannot edit/unlock locked knowledge. The support agent is restricted to its verified customer and cannot change shared knowledge from customer input. No borrowed human credentials.
|
|
40
42
|
|
|
@@ -51,3 +53,5 @@ Customer Inbox: Frontpage website chat uses one settings toggle, no website edit
|
|
|
51
53
|
## Emails workspace workflows
|
|
52
54
|
|
|
53
55
|
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. `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.
|
|
56
|
+
|
|
57
|
+
Group details Email and stage email tasks use the existing `frontpage mail save` / `save_mail_editor` completion. `attachment` checks saved group/stage emailVersion, creates paused rules, supports shared-template edits with rule revisions, and recovers identical retries. Read MAIL.md, Group and stage email tasks. Template get includes ruleUsage; deletion protects paused and active rule dependencies. Creating or saving never activates, enrolls or sends.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontpage-host",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Frontpage CLI
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Frontpage CLI — manage your site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"frontpage": "dist/bin.js"
|