frontpage-host 0.1.28 → 0.1.29
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 +5 -0
- package/CUSTOMER-INBOX.md +217 -0
- package/PEOPLE.md +34 -14
- package/dist/bin.js +3 -0
- package/dist/commands/customer-inbox.d.ts +14 -0
- package/dist/commands/customer-inbox.js +157 -0
- package/dist/commands/people-workspace.js +1 -2
- package/dist/commands/people.js +5 -1
- package/dist/registry.js +10 -1
- package/llms.txt +2 -0
- package/package.json +4 -3
package/AGENTS.md
CHANGED
|
@@ -256,3 +256,8 @@ frontpage doctor --json
|
|
|
256
256
|
## People groups, stages, rules, and imports
|
|
257
257
|
|
|
258
258
|
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`, and `company`. 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.
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
## Customer Inbox
|
|
262
|
+
|
|
263
|
+
Internal, allowlisted sites only. Use `frontpage customer-inbox get|put|post RESOURCE`, stable write keys and current versions. Full guide: [CUSTOMER-INBOX.md](./CUSTOMER-INBOX.md). `export CONVERSATION_ID --out FILE` writes authorized paginated history and file metadata; `download FILE_ID --out FILE` downloads through a file grant. Outputs never overwrite existing files. `frontpage usage` reports shared site credits, mail and storage. Human owners may handle conversations; website agents use `manage_customer_inbox` for setup/settings/widget/internal knowledge only, never a human CLI key to read or join customer threads.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Customer Inbox
|
|
2
|
+
|
|
3
|
+
Customer Inbox uses the site's existing AI credits and storage allowance. It is available on internal plans and approved pilot sites. The support agent uses fixed GPT-6 Luna and bills AI usage at 10× raw provider cost. Human handling does not call a model for each message.
|
|
4
|
+
|
|
5
|
+
Run `frontpage help customer-inbox`. Use the site's normal CLI key:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
frontpage customer-inbox get status --site my-site
|
|
9
|
+
frontpage customer-inbox get settings --site my-site
|
|
10
|
+
frontpage customer-inbox put settings --file ./settings.json --request-key settings_revision_1 --site my-site
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Reads use `get`. Writes use `put` or `post`, a UTF-8 JSON object file, and an 8–128 character request key. Reuse the same key and body after a failed or lost response. Changed bodies need new keys. The service rejects a key reused for a different request. Read current versions before changing records.
|
|
14
|
+
|
|
15
|
+
## Setup and settings
|
|
16
|
+
|
|
17
|
+
| Resource | Method | JSON body |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `status` | GET | None |
|
|
20
|
+
| `activation` | PUT | `{"enabled":true}`; admin only |
|
|
21
|
+
| `address` | GET | Returns the current address, domain and policy version |
|
|
22
|
+
| `address` | PUT | `{"alias":"acme","policyVersion":"2026-10-13.1"}`; admin only |
|
|
23
|
+
| `address` | POST | `{"retire":true}`; admin only |
|
|
24
|
+
| `settings` | GET | Returns `version`, `value`, fixed model and multiplier |
|
|
25
|
+
| `settings` | PUT | `{"expectedVersion":0,"value":{"instructions":"Ask me if unsure."}}` |
|
|
26
|
+
|
|
27
|
+
Setup binds private guidance to the verified owner who requested it. That identity comes from Core, never model/customer fields. The agent manages new conversations by default after setup; the platform's rollout switches remain off until pilot verification passes. Set `value.enabled:false` for manual handling. Settings changes cancel stale automatic jobs and queued automatic sends.
|
|
28
|
+
|
|
29
|
+
Settings allow `enabled`, `instructions`, `tools`, `maxSteps` (1–12), `maxOutputTokens` (256–2400), and `maxInputBytes` (4000–64000). Supported tools: `knowledge_read`, `knowledge_update`, `customer_read`, `customer_update`, `notes_read`, `note_add`, `ask_owner`, `reply_customer`, `wait`, `finish`. Keep `wait` and `finish` enabled. Defaults: 8 steps, 1200 output tokens per call, 32000 bytes of public text. Model, billing multiplier and recipients are not caller-selectable fields. The worker also enforces platform concurrency, deadlines and input limits.
|
|
30
|
+
|
|
31
|
+
Aliases use 3–48 ASCII letters, numbers and single hyphens. The service normalizes case and checks collisions in one transaction. Reserved platform names and routing namespaces cannot be claimed. Retired addresses remain reserved; they never move to another site.
|
|
32
|
+
|
|
33
|
+
## Internal knowledge and approved sources
|
|
34
|
+
|
|
35
|
+
| Resource | Method | JSON body |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `knowledge` | GET | Recent notes; optional `?q=search` |
|
|
38
|
+
| `knowledge/<id>` | GET | One full note |
|
|
39
|
+
| `knowledge/<id>/history` | GET | Immutable revisions for that note |
|
|
40
|
+
| `knowledge` | PUT | `{"title":"Evening visits","body":"Ask the owner before promising an evening visit.","expectedVersion":0,"reason":"Owner policy"}` |
|
|
41
|
+
| `knowledge/<id>` | PUT | Current title/body, `expectedVersion`, `reason`, optional `locked` or `restoreVersion` |
|
|
42
|
+
| `sources` | GET | Approved source records |
|
|
43
|
+
| `sources` | PUT | `{"url":"https://example.com/help","enabled":true,"expectedVersion":0}`; fetches public text |
|
|
44
|
+
|
|
45
|
+
Supply `body` on a source write to save an owner-approved excerpt instead of fetching. Published sources refresh when the agent reads a source older than 24 hours. The fetcher reuses Core's DNS-pinned public HTTP infrastructure, rejects private addresses, checks redirects and bounds content. Sources over 16000 text characters require an owner-approved excerpt. No scripts run. A source version/digest fences knowledge updates against a changed source.
|
|
46
|
+
|
|
47
|
+
Knowledge search uses bounded literal keywords from natural-language queries and ranks notes by matched terms. Read the full matching note before updating it; update its current ID/version instead of creating a conflicting duplicate.
|
|
48
|
+
|
|
49
|
+
Lock a knowledge note with `locked:true`. The support agent cannot change locked notes or locks. Restore an older note with `restoreVersion`; the service creates a new revision and preserves the old history. The agent can change only internal knowledge, using verified owner guidance or a current approved source. It cannot edit public pages or articles.
|
|
50
|
+
|
|
51
|
+
## Human conversation handling
|
|
52
|
+
|
|
53
|
+
| Resource | Method | JSON body |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `conversations` | GET | Paginated list; `?limit=50&after=<id>` |
|
|
56
|
+
| `conversations/<id>` | GET | Thread and latest public message |
|
|
57
|
+
| `conversations/<id>/messages` | GET | `?after=0&limit=50&audience=all`; viewers get public history only |
|
|
58
|
+
| `conversations/<id>/routing` | GET | Current handler/version, People link and delivery states |
|
|
59
|
+
| `conversations/<id>/reply` | POST | `{"text":"Tuesday at 6:30 works. Keep staffing private.","handlerVersion":0,"privateGuidance":true}` |
|
|
60
|
+
| `conversations/<id>/control` | POST | `{"action":"takeover","handlerVersion":0}` or `{"action":"return","handlerVersion":1}` |
|
|
61
|
+
| `conversations/<id>/reply` | POST | `{"text":"Your quote is ready.","handlerVersion":1}`; requires current owner control |
|
|
62
|
+
| `files?messageId=<id>` | GET | Bound message files; editor/admin only |
|
|
63
|
+
| `files/<id>` | GET | Short-lived download grant; editor/admin only |
|
|
64
|
+
|
|
65
|
+
Private guidance stays in the same internal thread. The agent asks by a private email and stores its question in the history. Replying by email or UI gives context and leaves the agent responsible for the customer. The agent uses that context to answer and can update unlocked internal knowledge.
|
|
66
|
+
|
|
67
|
+
Email replies route through opaque, recipient-bound Reply-To addresses linked to the site, conversation, purpose, owner and handler version. Subjects and message text never choose a customer thread. The service checks current owner membership and sender authentication before accepting private owner mail. To take over by email, reply only `Take over this conversation`. The owner receives the customer-visible history and replies through Inbox. If history exceeds the email limit, the notification links to the full paginated history/export; it never claims a shortened transcript is complete. To return control, reply only `Agent, take it from here`. Mixed or unclear handoff text stays private for review. Customer and owner addresses are never added to a shared CC thread by Inbox.
|
|
68
|
+
|
|
69
|
+
Manual customer/owner exchanges use ordinary routing code. No support inference runs for every manual message. Returning control checks only eligible unanswered input and suppresses stale automatic sends.
|
|
70
|
+
|
|
71
|
+
Set `enabled:false` in settings to use manual handling for new conversations. The service binds that relay to the configured verified owner. Existing agent threads switch to manual handling on the next customer message or public owner reply. Re-enable the agent before returning a thread to it. Private guidance alone always stays private and never transfers control.
|
|
72
|
+
|
|
73
|
+
Set `guidanceToMe:true` in a versioned settings update to bind the signed current owner's verified address. Arbitrary recipient addresses and user IDs remain forbidden. The service saves only the resolved owner identity, not this setup flag.
|
|
74
|
+
|
|
75
|
+
## Security review and recovery
|
|
76
|
+
|
|
77
|
+
| Resource | Method | JSON body |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `blocks` | GET | Site-scoped block evidence references; human editor/admin only |
|
|
80
|
+
| `blocks/<id>/unblock` | POST | `{"version":1}` |
|
|
81
|
+
| `jobs` | GET | Job state/reason and current `updatedAt` |
|
|
82
|
+
| `jobs/<id>/resume` | POST | `{"updatedAt":"<current ISO time>"}` for recoverable budget, receipt, rate or service pauses |
|
|
83
|
+
| `receipts` | GET | Held inbound mail; admin only |
|
|
84
|
+
| `receipts/<id>` | POST | `{}` queues a held capture retry; admin/pilot only |
|
|
85
|
+
|
|
86
|
+
Only a validated clear attack can create an automatic site-scoped block. Authenticated customer mail binds a verified email identity; unverified mail stays thread-scoped. Blocks never ban a People contact across sites or a shared IP. Unblock preserves evidence and does not replay the attack. Uncertain, malformed, refused or unavailable screening pauses the agent and leaves human handling available. Known blocks and rate/size checks stop work before another paid call.
|
|
87
|
+
|
|
88
|
+
The fixed policy requires allow probability at least 0.90 and confidence at least 0.80, with no attack category. Automatic blocking requires malicious probability and confidence at least 0.98 and an attack category. Other results pause for human review; a review does not label the customer malicious. These are platform controls, not customer settings. Review email notices deduplicate per conversation and reason group per hour. Saved receipt retries wait at least 30 seconds so one failed receipt cannot monopolize recovery.
|
|
89
|
+
|
|
90
|
+
Receipt recovery resubmits saved provider usage to the existing ledger. It never repeats inference. An interrupted request with unknown completion requires review. Resume cannot bypass screening, replay a blocked attack, or pretend unknown usage is zero. Exact input/output/cache/reasoning counts stay unchanged; the ledger records raw cost, fixed multiplier, customer debit, job/call/conversation references and immutable prices.
|
|
91
|
+
|
|
92
|
+
## Website-agent management boundary
|
|
93
|
+
|
|
94
|
+
The website agent uses `manage_customer_inbox` and `get_customer_inbox_docs`, with a signed manager principal bound to the owner run. It manages settings, tools, aliases and internal knowledge. It cannot read live threads or files, send messages, provide private guidance, take over/return, or review customer block evidence. Server checks enforce this even through indirect paths. Never give the website agent a human CLI/API key. Its manager tool uses no human keychain credential.
|
|
95
|
+
|
|
96
|
+
The developer guide lives at `https://frontpage.host/developers/customer-inbox`. Changes to that site use the Frontpage CLI pull/push/deploy/publish process.
|
|
97
|
+
|
|
98
|
+
## Owner workspace and widget
|
|
99
|
+
|
|
100
|
+
Open `/workspace/sites/<siteId>/customer-inbox` on an approved internal site. The workspace uses the same versioned service operations as the CLI. Conversation rows show the latest public message. Customer messages, agent replies, owner replies, private guidance, notes and handling changes have distinct labels. Private guidance stays in the thread and does not transfer ownership.
|
|
101
|
+
|
|
102
|
+
Use `frontpage customer-inbox get widget --site my-site` as a human admin to get the script URL, site ID and server-side app identity key. Install the returned script through the normal Frontpage CLI file workflow or your app's template:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<script defer src="https://app.frontpage.host/customer-inbox/widget.js" data-site="SITE_UUID"></script>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The current scaffold and gateway already allow this HTTPS script and its requests. Installation uses their existing script support; it needs no gateway change, scaffold upgrade or fleet rebuild. The widget stays separate from the site bundle. It appears only when the service accepts that site's activation, rollout gates and approved origin.
|
|
109
|
+
|
|
110
|
+
Update settings using the current `expectedVersion`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"expectedVersion": 4,
|
|
115
|
+
"value": {
|
|
116
|
+
"widget": {
|
|
117
|
+
"enabled": true,
|
|
118
|
+
"title": "Message our team",
|
|
119
|
+
"greeting": "How can we help?",
|
|
120
|
+
"color": "#7254d6",
|
|
121
|
+
"requireName": true,
|
|
122
|
+
"requireEmail": true,
|
|
123
|
+
"origins": ["https://my-site.frontpage.host", "https://app.example.com"]
|
|
124
|
+
},
|
|
125
|
+
"ownerCopies": false,
|
|
126
|
+
"helpRules": {
|
|
127
|
+
"humanRequest": true,
|
|
128
|
+
"missingAnswer": true,
|
|
129
|
+
"frustration": true,
|
|
130
|
+
"repeatedFailure": true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use exact HTTPS origins without paths or credentials. This controls cross-origin browser access, not owner permissions. Required intake fields collect details; they do not prove email ownership. The customer must confirm the emailed link before email continuation or authenticated People access. Confirmation rotates the visitor credential. Recovery uses a new email proof and returns no history based on a typed address alone.
|
|
137
|
+
|
|
138
|
+
The agent reads help rules in its support prompt and uses `ask_owner`. Work caps remain server-enforced. Owner copies are private emails of future agent replies. Replying gives private guidance unless the owner explicitly requests takeover. Neither copies nor guidance put owner and customer on a shared CC thread.
|
|
139
|
+
|
|
140
|
+
### Signed app identity
|
|
141
|
+
|
|
142
|
+
Keep the `identityKey` returned by `widget` on your app server. Never include it in HTML, browser storage or a client bundle. Sign a short-lived assertion after authenticating your app user:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { createHmac } from "node:crypto";
|
|
146
|
+
const now = Math.floor(Date.now() / 1000);
|
|
147
|
+
const claims = {
|
|
148
|
+
siteId: process.env.INBOX_SITE_ID,
|
|
149
|
+
origin: "https://app.example.com",
|
|
150
|
+
subject: authenticatedUser.id,
|
|
151
|
+
email: authenticatedUser.email,
|
|
152
|
+
name: authenticatedUser.name,
|
|
153
|
+
iat: now,
|
|
154
|
+
exp: now + 120
|
|
155
|
+
};
|
|
156
|
+
const encoded = Buffer.from(JSON.stringify(claims)).toString("base64url");
|
|
157
|
+
const identity = encoded + "." + createHmac("sha256", process.env.INBOX_IDENTITY_KEY)
|
|
158
|
+
.update(encoded).digest("hex");
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The service verifies the signature, site, origin and expiry. Assertions can last at most five minutes. They grant customer identity only, never owner or manager authority. Every app-session request must include the current signed identity matching that session's subject and email. Put a server-rendered assertion in `data-identity`, or preferably provide `data-identity-url="/api/support-identity"`. That same-origin HTTPS endpoint authenticates the current app user and returns `{"identity":"<signed assertion>"}`. The widget refreshes it; after logout or account changes, old app sessions cannot be read without the matching identity. Anonymous widgets need no assertion. Each approved origin has its own visitor session; email continues the bound conversation.
|
|
162
|
+
|
|
163
|
+
## Conversation state, exports and shared usage
|
|
164
|
+
|
|
165
|
+
| Resource | Method | Body or behavior |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `conversations?state=active` | GET | Open threads; also `closed`, `trash` or `all` |
|
|
168
|
+
| `conversations/<id>/messages?latest=true&limit=50&audience=all` | GET | Latest page in message order |
|
|
169
|
+
| `conversations/<id>/messages?before=<sequence>&limit=50&audience=all` | GET | Earlier page; do not combine with `after` or `latest` |
|
|
170
|
+
| `conversations/<id>/state` | POST | `{"action":"close","expectedVersion":0}`; actions: `close`, `reopen`, `trash`, `restore`, `read` |
|
|
171
|
+
| `conversations/<id>/state` | POST | Mark read with current state version and `"sequence":42` |
|
|
172
|
+
| `conversations/<id>/revoke-sessions` | POST | `{}`; revoke existing visitor credentials |
|
|
173
|
+
|
|
174
|
+
Trash retains history and file references. Restore reopens the thread; it does not erase prior handling decisions. State writes reject stale versions. Closing cancels queued sends and refuses unresolved provider outcomes. New customer input reopens a closed thread; trashed threads require owner restoration. Visitor revocation also disables recovery of that session.
|
|
175
|
+
|
|
176
|
+
Export the full authorized history and file manifest, one JSON record per line:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
frontpage customer-inbox export CONVERSATION_UUID --out ./conversation.jsonl --site my-site
|
|
180
|
+
frontpage customer-inbox download FILE_UUID --out ./attachment.png --site my-site
|
|
181
|
+
frontpage usage --site my-site --json
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Export uses paginated reads rather than loading all messages into memory. Download uses an authorized file grant. Both commands refuse to overwrite an existing output file. Full private-history export and file access require human editor/admin authority. Viewers can read public history through the normal message resource.
|
|
185
|
+
|
|
186
|
+
Inbox sends count in the site's existing email allowance. Provider-accepted send receipts settle that usage once, including private owner notifications and copies. AI calls debit the site's existing credit ledger at 10× cost, with token counts unchanged. Storage uses the existing Blob scanner and counts actual attachments, originals and previews; conversation text adds no separate meter.
|
|
187
|
+
|
|
188
|
+
## File custody and cleanup
|
|
189
|
+
|
|
190
|
+
Email capture keeps the original MIME message, attachments and provider receipt. Email and chat image uploads keep the original file and a smaller agent preview. Repeated upload/message receipts retain the same file/message IDs. Validated raster customer images up to 12 megapixels use the existing public bucket. Larger or undecodable files remain downloadable and require human review. Other files and private mail originals use authenticated downloads over encrypted bytes in that same bucket. No Blob firewall changes or separate bucket are part of this release.
|
|
191
|
+
|
|
192
|
+
The existing backup process enumerates the whole bucket. The existing reaper scans Inbox files in bounded pages and asks Inbox for exact retained references; it does not read message bodies. Unknown reference results protect Inbox files while legacy cleanup continues. Live history, Trash, capture holds and original/preview links retain their files. The existing archive/restore/purge coordinator claims a file in Inbox before changing bytes. A claim blocks new links and remains held after an unknown outcome. Accepted email custody, message links, Trash and capture holds protect files; abandoned chat uploads can follow the existing grace/archive rules. Unknown file records stay protected. Site finalization freezes Inbox writes before moving its bytes through the same archive process. Broader shared-reaper performance work remains a later phase.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
On app logout or account change, call `window.dispatchEvent(new Event("frontpage-inbox:reset"))`. This clears the displayed history, stored chat credential and pending input. The next request fetches a fresh assertion from `data-identity-url`; authenticated app sessions require the same current app identity on every request. Do not use a long-lived static assertion for an app with account switching. Verification returns to the same origin and the stored page path. Query strings and arbitrary redirect URLs are not accepted.
|
|
196
|
+
|
|
197
|
+
`jobs/<id>` reads one human-visible recovery state. Retry only the documented recoverable pauses using its current `updatedAt`; invalid screening and unknown sends require human review. An unknown shared-media operation keeps its `claimId`, `claimAction` and `lifecycleVersion`. Operators verify live/archive byte hashes and the existing archive row before acknowledging the matching service-only `/media-finish` operation. Never clear a claim solely because time passed. These endpoints accept only the `inbox-media` service actor; owner keys and website managers cannot submit them.
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## Support agent tool contract
|
|
201
|
+
|
|
202
|
+
The support agent has its own system prompt plus the versioned private business instructions. Tool calls stay in Inbox's tenant, conversation, handler, configuration and job context. All ten tools start enabled; `wait` and `finish` cannot be removed. A disabled tool fails at the server.
|
|
203
|
+
|
|
204
|
+
| Tool | Inputs | Effect and limits |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| `knowledge_read` | `query`, `id` or `sourceId` | Search internal notes or read complete approved evidence. |
|
|
207
|
+
| `knowledge_update` | Optional `id`; `title`, `body`, `expectedVersion`, `reason`, `evidenceKind`, `evidenceId`; source `evidenceVersion` | Update unlocked internal facts from verified guidance in this thread or an approved source revision. One-off guidance stays in the conversation. No public editing. |
|
|
208
|
+
| `customer_read` | None | Read only this thread's verified People contact. |
|
|
209
|
+
| `customer_update` | `name`/`phone`, `evidenceMessageId` | Enrich from an explicit customer statement; cannot change identity or permission. |
|
|
210
|
+
| `notes_read` | None | Read factual People notes for the bound contact. |
|
|
211
|
+
| `note_add` | `text` | Save a private factual action note in People, with an idempotent receipt. Do not store hidden reasoning. |
|
|
212
|
+
| `ask_owner` | `question` | Save the private question in the thread, email the verified configured owner and pause. Guidance does not take over. |
|
|
213
|
+
| `reply_customer` | `text` | Save a public answer and queue email to the bound verified customer when available. Never choose recipients. |
|
|
214
|
+
| `wait` | `reason` | Pause safely without sending. |
|
|
215
|
+
| `finish` | None | Complete this bounded job. |
|
|
216
|
+
|
|
217
|
+
Decisions screens malicious input only. It never chooses customer recipients, business answers, tools, owner handling or knowledge changes. Shared inference utilities provide the existing same-model credential/transport fallback; no expensive model substitution or customer model picker is added.
|
package/PEOPLE.md
CHANGED
|
@@ -411,6 +411,23 @@ For a reviewed cohort, submit at most 25 people in each bounded batch. Each row
|
|
|
411
411
|
|
|
412
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 works through the public API and `manage_people` too; it has no company-only endpoint.
|
|
413
413
|
|
|
414
|
+
## Safe sync through public customer commands
|
|
415
|
+
|
|
416
|
+
Integrations read their own sources and write through the same CLI/API/agent operations as owners. No product database write credential is needed.
|
|
417
|
+
|
|
418
|
+
- `person.lookup` takes a valid `email` and/or `phone`, includes hidden matches, and refuses identities that point to different people. It never changes permission or unhides someone.
|
|
419
|
+
- `person.update` takes `personId`, a `patch` and optional `expected` contact values. `expected` supports exact name/email/phone values, including null. A changed value or hidden contact returns a conflict; it does not overwrite the owner's edit. Use `people workspace person.update --data` for this conditional operation.
|
|
420
|
+
- `notes.add` takes `personId`, `body`, optional `requestKey` (1–120 characters), and optional `historical` boolean. Reuse the key for the exact same note after a lost response. A changed person/body/history flag returns a conflict. A retry never restores a deleted note. Historical notes skip automation and record their processed receipt immediately.
|
|
421
|
+
- `membership.add` accepts optional `historical:true` to import a membership without replaying rules. Ordinary additions keep normal automation. Preserve an owner's removed membership rather than adding it again on every sync.
|
|
422
|
+
|
|
423
|
+
```sh
|
|
424
|
+
frontpage people workspace person.lookup --data '{"email":"alex@example.com"}' --site YOUR_SITE --json
|
|
425
|
+
frontpage people workspace person.update --data '{"personId":"PERSON_ID","patch":{"phone":"+12025550100"},"expected":{"phone":null}}' --site YOUR_SITE --json
|
|
426
|
+
frontpage people workspace notes.add --data '{"personId":"PERSON_ID","body":"Verified billing context","requestKey":"NOTE_UUID","historical":true}' --site YOUR_SITE --json
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Use a saved CSV import for a new historical cohort. Its list-wide permission choice applies only to new contacts; row values take priority and existing decisions remain saved. Check the real import status before writing notes or starting delivery. Source account or billing facts never imply marketing permission. Preserve hidden state and review identity conflicts rather than merging contacts.
|
|
430
|
+
|
|
414
431
|
## Import a customer list from CSV
|
|
415
432
|
|
|
416
433
|
CSV imports accept up to 5 MB and 10,000 contact rows. Quoted commas, quoted newlines, UTF-8 BOM, and CRLF are supported. Larger lists need separate files; the UI does not render an unbounded list of failures.
|
|
@@ -508,18 +525,6 @@ frontpage mail campaign get --id DRAFT_ID --site YOUR_SITE --json
|
|
|
508
525
|
|
|
509
526
|
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.
|
|
510
527
|
|
|
511
|
-
## Frontpage account sync
|
|
512
|
-
|
|
513
|
-
This admin-only feature adds Frontpage’s own account holders to the `landing` People workspace and keeps later signups in sync. It is not available to ordinary businesses. Businesses import their customer lists with CSV.
|
|
514
|
-
|
|
515
|
-
```
|
|
516
|
-
frontpage people company get --site landing --json
|
|
517
|
-
frontpage people company set --enabled true --site landing --json
|
|
518
|
-
frontpage people company set --enabled false --site landing --json
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
Sync uses saved batches and Frontpage user IDs to avoid duplicates. The backfill does not fire rules or grant email permission. Later signups can trigger group entry rules, subject to normal eligibility. Sync preserves owner edits. Pausing it leaves existing contacts saved. Supply an existing `groupId` through `--data` if you want a particular sync group.
|
|
522
|
-
|
|
523
528
|
## CLI, API, and agent reference
|
|
524
529
|
|
|
525
530
|
The editor, public API, CLI, and site agent call the same application service. Every operation below is available without opening the UI.
|
|
@@ -612,8 +617,6 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
612
617
|
| import.start | csv, mapping?, groupIds?, requestKey | Start a saved import. |
|
|
613
618
|
| import.get | id | Read the import cursor, outcomes, and errors. |
|
|
614
619
|
| import.retry | id | Retry failed rows after correcting saved identity conflicts. |
|
|
615
|
-
| company.get | — | Read the admin-only Frontpage account sync. |
|
|
616
|
-
| company.set | enabled, groupId? | Start or pause that sync in landing; platform admin only. |
|
|
617
620
|
|
|
618
621
|
## Limits, timing, and recovery
|
|
619
622
|
|
|
@@ -628,3 +631,20 @@ The agent can create and edit saved templates and series with the existing Email
|
|
|
628
631
|
**Keep scope clear.**
|
|
629
632
|
|
|
630
633
|
People does not replace the raw Inbox, Store orders, or the email send ledger. It combines the contact’s history and organization. Those source tools and their existing CLI commands remain available.
|
|
634
|
+
|
|
635
|
+
## Manual saved emails from People
|
|
636
|
+
|
|
637
|
+
Templates store reusable content; series store ordered templates and delays. Neither definition stores a customer audience. Their existing API/CLI sends and rule callers remain supported. In People, use **Email → Use a template** to create an independent campaign draft with the current audience. Review the draft before sending; editing it never changes its source template. A template's **Create campaign** shortcut copies saved content, then asks for a campaign audience.
|
|
638
|
+
|
|
639
|
+
The shared workspace operations are:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
frontpage people workspace template.campaign --site YOUR_SITE --data '{"templateId":"TEMPLATE_ID","requestKey":"YOUR_STABLE_KEY","audience":{"personIds":["PERSON_ID"]}}' --json
|
|
643
|
+
frontpage people workspace series.preview --site YOUR_SITE --data '{"seriesId":"SERIES_ID","personIds":["PERSON_ID"]}' --json
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
To enrol, copy each returned batch's `personIds` and `reviewToken` into `series.enrol`, adding `seriesId` and a unique stable `requestKey`. Preview returns batches of five; the service accepts at most 25 per request. Reuse the original payload and key after a lost response. Results distinguish enrolled, already enrolled, skipped and failed. A new preview uses new keys. Never enrol or send without the user's explicit instruction.
|
|
647
|
+
|
|
648
|
+
Manual series starts use selected person IDs, an active series and the normal queue. Confirmation queues its first email; saved delays schedule later steps. The review lists permission exclusions and remaining allowance. It never activates a paused series or restarts an existing enrolment, including one bound to a person whose email changed. Delivery rechecks saved restrictions. Contact groups and permission stay unchanged.
|
|
649
|
+
|
|
650
|
+
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.
|
package/dist/bin.js
CHANGED
|
@@ -26,6 +26,7 @@ import { runExtensions } from "./commands/extensions.js";
|
|
|
26
26
|
import { runMedia } from "./commands/media.js";
|
|
27
27
|
import { runImages } from "./commands/images.js";
|
|
28
28
|
import { runInbox } from "./commands/inbox.js";
|
|
29
|
+
import { runCustomerInbox } from "./commands/customer-inbox.js";
|
|
29
30
|
import { runSeo } from "./commands/seo.js";
|
|
30
31
|
import { runRedirects } from "./commands/redirects.js";
|
|
31
32
|
import { runPeople } from "./commands/people.js";
|
|
@@ -117,6 +118,8 @@ async function main() {
|
|
|
117
118
|
return runImages(args);
|
|
118
119
|
case "inbox":
|
|
119
120
|
return runInbox(args);
|
|
121
|
+
case "customer-inbox":
|
|
122
|
+
return runCustomerInbox(args);
|
|
120
123
|
case "people":
|
|
121
124
|
return runPeople(args);
|
|
122
125
|
case "routines":
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function inboxCommand(args: string[]): {
|
|
2
|
+
method: string;
|
|
3
|
+
resource: string;
|
|
4
|
+
file: undefined;
|
|
5
|
+
key: undefined;
|
|
6
|
+
out: string;
|
|
7
|
+
} | {
|
|
8
|
+
method: string;
|
|
9
|
+
resource: string;
|
|
10
|
+
file: string | undefined;
|
|
11
|
+
key: string | undefined;
|
|
12
|
+
out: undefined;
|
|
13
|
+
};
|
|
14
|
+
export declare function runCustomerInbox(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFile, open, link, unlink } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { resolveSlug } from "../site.js";
|
|
4
|
+
import { takeFlag, assertNoUnknownFlags } from "../argv.js";
|
|
5
|
+
import { getApiKey } from "../auth/keychain.js";
|
|
6
|
+
import { apiBase, v1Get, httpErrorFromV1, CliHttpError, EXIT_AUTH, EXIT_USAGE, EXIT_OK, exitCodeForError, } from "../http/client.js";
|
|
7
|
+
import { emitOk, emitErr, isJson } from "../runtime.js";
|
|
8
|
+
const RESOURCE = /^(?:status|activation|address|widget|settings|knowledge(?:\/[0-9a-f-]+(?:\/history)?)?|sources|blocks(?:\/[0-9a-f-]+\/unblock)?|jobs(?:\/[0-9a-f-]+(?:\/resume)?)?|files(?:\/[0-9a-f-]+)?|receipts(?:\/[0-9a-f-]+)?|conversations(?:\/[0-9a-f-]+(?:\/(?:messages|routing|guidance|reply|control|state|revoke-sessions))?)?)(?:\?[^#]*)?$/;
|
|
9
|
+
export function inboxCommand(args) {
|
|
10
|
+
const work = [...args], verb = work.shift(), resource = work.shift(), file = takeFlag(work, "--file"), key = takeFlag(work, "--request-key"), out = takeFlag(work, "--out");
|
|
11
|
+
assertNoUnknownFlags(work, "customer-inbox");
|
|
12
|
+
if (verb === "export" || verb === "download") {
|
|
13
|
+
if (work.length ||
|
|
14
|
+
!resource ||
|
|
15
|
+
!out ||
|
|
16
|
+
file ||
|
|
17
|
+
key ||
|
|
18
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(resource))
|
|
19
|
+
throw new CliHttpError("Use customer-inbox export|download <id> --out <new-file>.", EXIT_USAGE, 400, "bad_request");
|
|
20
|
+
return {
|
|
21
|
+
method: verb.toUpperCase(),
|
|
22
|
+
resource,
|
|
23
|
+
file: undefined,
|
|
24
|
+
key: undefined,
|
|
25
|
+
out,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (out ||
|
|
29
|
+
work.length ||
|
|
30
|
+
!verb ||
|
|
31
|
+
!["get", "put", "post"].includes(verb) ||
|
|
32
|
+
!resource ||
|
|
33
|
+
!RESOURCE.test(resource))
|
|
34
|
+
throw new CliHttpError("Usage: frontpage customer-inbox get|put|post <resource> [--file <json>] [--request-key <key>] [--site <slug>]", EXIT_USAGE, 400, "bad_request");
|
|
35
|
+
if (verb !== "get" && (!file || !key || !/^[A-Za-z0-9_-]{8,128}$/.test(key)))
|
|
36
|
+
throw new CliHttpError("Writes require --file and an 8–128 character --request-key. Reuse both on retries.", EXIT_USAGE, 400, "bad_request");
|
|
37
|
+
if (verb === "get" && (file || key))
|
|
38
|
+
throw new CliHttpError("Reads do not take --file or --request-key.", EXIT_USAGE, 400, "bad_request");
|
|
39
|
+
return { method: verb.toUpperCase(), resource, file, key, out: undefined };
|
|
40
|
+
}
|
|
41
|
+
export async function runCustomerInbox(args) {
|
|
42
|
+
try {
|
|
43
|
+
const { slug, rest } = await resolveSlug(args), operation = inboxCommand(rest), path = `/api/v1/sites/${encodeURIComponent(slug)}/customer-inbox/${operation.resource}`;
|
|
44
|
+
let result;
|
|
45
|
+
if (operation.method === "EXPORT" || operation.method === "DOWNLOAD") {
|
|
46
|
+
const target = operation.out, temp = target + ".inbox-" + randomUUID() + ".tmp", handle = await open(temp, "wx", 0o600);
|
|
47
|
+
let count = 0;
|
|
48
|
+
try {
|
|
49
|
+
const prefix = `/api/v1/sites/${encodeURIComponent(slug)}/customer-inbox`;
|
|
50
|
+
if (operation.method === "DOWNLOAD") {
|
|
51
|
+
const record = await v1Get(`${prefix}/files/${operation.resource}`);
|
|
52
|
+
const response = await fetch(record.data.downloadUrl, {
|
|
53
|
+
redirect: "error",
|
|
54
|
+
signal: AbortSignal.timeout(60000),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok || !response.body)
|
|
57
|
+
throw new CliHttpError("Attachment download failed.", EXIT_USAGE, response.status, "download_failed");
|
|
58
|
+
for await (const chunk of response.body) {
|
|
59
|
+
count += chunk.byteLength;
|
|
60
|
+
if (count > 25 * 1024 * 1024)
|
|
61
|
+
throw new CliHttpError("Attachment exceeds 25 MB.", EXIT_USAGE, 413, "payload_too_large");
|
|
62
|
+
await handle.writeFile(chunk);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const conversation = await v1Get(`${prefix}/conversations/${operation.resource}`);
|
|
67
|
+
await handle.writeFile(JSON.stringify({ type: "conversation", value: conversation.data }) +
|
|
68
|
+
"\n");
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const page = await v1Get(`${prefix}/conversations/${operation.resource}/messages?after=${cursor}&limit=100&audience=all`);
|
|
72
|
+
for (const message of page.data.messages) {
|
|
73
|
+
const attachments = await v1Get(`${prefix}/files?messageId=${message.id}`);
|
|
74
|
+
await handle.writeFile(JSON.stringify({
|
|
75
|
+
type: "message",
|
|
76
|
+
value: message,
|
|
77
|
+
attachments: attachments.data,
|
|
78
|
+
}) + "\n");
|
|
79
|
+
count++;
|
|
80
|
+
}
|
|
81
|
+
if (page.data.nextCursor === null)
|
|
82
|
+
break;
|
|
83
|
+
if (page.data.nextCursor <= cursor)
|
|
84
|
+
throw new Error("History cursor did not advance.");
|
|
85
|
+
cursor = page.data.nextCursor;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await handle.sync();
|
|
89
|
+
await handle.close();
|
|
90
|
+
await link(temp, target);
|
|
91
|
+
await unlink(temp);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
await handle.close().catch(() => { });
|
|
95
|
+
await unlink(temp).catch(() => { });
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
result = {
|
|
99
|
+
path: target,
|
|
100
|
+
...(operation.method === "EXPORT"
|
|
101
|
+
? { messages: count, format: "jsonl" }
|
|
102
|
+
: { bytes: count }),
|
|
103
|
+
};
|
|
104
|
+
if (isJson())
|
|
105
|
+
emitOk("customer-inbox", result);
|
|
106
|
+
else
|
|
107
|
+
console.log(JSON.stringify(result, null, 2));
|
|
108
|
+
return EXIT_OK;
|
|
109
|
+
}
|
|
110
|
+
if (operation.method === "GET")
|
|
111
|
+
result = await v1Get(path, { timeoutMs: 20000 });
|
|
112
|
+
else {
|
|
113
|
+
const bytes = await readFile(operation.file);
|
|
114
|
+
if (bytes.length > 65536)
|
|
115
|
+
throw new CliHttpError("Inbox JSON exceeds 64 KiB.", EXIT_USAGE, 413, "payload_too_large");
|
|
116
|
+
let value;
|
|
117
|
+
try {
|
|
118
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new CliHttpError("Use a valid UTF-8 JSON object.", EXIT_USAGE, 400, "bad_request");
|
|
122
|
+
}
|
|
123
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
124
|
+
throw new CliHttpError("Use a JSON object.", EXIT_USAGE, 400, "bad_request");
|
|
125
|
+
const token = await getApiKey();
|
|
126
|
+
if (!token)
|
|
127
|
+
throw new CliHttpError("Not logged in. Run: frontpage login", EXIT_AUTH, 401);
|
|
128
|
+
const response = await fetch(apiBase() + path, {
|
|
129
|
+
method: operation.method,
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${token}`,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
"Idempotency-Key": operation.key,
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify(value),
|
|
136
|
+
redirect: "error",
|
|
137
|
+
signal: AbortSignal.timeout(20000),
|
|
138
|
+
});
|
|
139
|
+
const body = (await response.json());
|
|
140
|
+
if (!response.ok)
|
|
141
|
+
throw httpErrorFromV1(response.status, body);
|
|
142
|
+
result = body;
|
|
143
|
+
}
|
|
144
|
+
if (isJson())
|
|
145
|
+
emitOk("customer-inbox", result);
|
|
146
|
+
else
|
|
147
|
+
console.log(JSON.stringify(result, null, 2));
|
|
148
|
+
return EXIT_OK;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (isJson())
|
|
152
|
+
emitErr("customer-inbox", e);
|
|
153
|
+
else
|
|
154
|
+
console.error(e instanceof Error ? e.message : "Inbox request failed.");
|
|
155
|
+
return exitCodeForError(e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -10,7 +10,6 @@ const AREAS = [
|
|
|
10
10
|
"import",
|
|
11
11
|
"attachments",
|
|
12
12
|
"person",
|
|
13
|
-
"company",
|
|
14
13
|
];
|
|
15
14
|
export function isPeopleWorkspaceCommand(sub) {
|
|
16
15
|
return (!!sub &&
|
|
@@ -23,7 +22,7 @@ export async function runPeopleWorkspace(slug, args) {
|
|
|
23
22
|
? work.shift()
|
|
24
23
|
: area === "overview" || area === "sources"
|
|
25
24
|
? area
|
|
26
|
-
: `${area}.${work.shift() ?? (area === "
|
|
25
|
+
: `${area}.${work.shift() ?? (area === "audience" ? "preview" : "list")}`;
|
|
27
26
|
const file = takeFlag(work, "--file"), json = takeFlag(work, "--data");
|
|
28
27
|
if (file && json)
|
|
29
28
|
throw new CliHttpError("Use --file or --data, not both.", EXIT_USAGE, 0, "bad_request");
|
package/dist/commands/people.js
CHANGED
|
@@ -232,12 +232,16 @@ export async function runPeople(args) {
|
|
|
232
232
|
const work = rest.slice(1);
|
|
233
233
|
const id = requireId(work);
|
|
234
234
|
const body = takeFlag(work, "--body") ?? takeFlag(work, "--note");
|
|
235
|
+
const requestKey = takeFlag(work, "--request-key");
|
|
236
|
+
const historical = takeFlag(work, "--historical");
|
|
237
|
+
if (historical !== undefined && !["true", "false"].includes(historical))
|
|
238
|
+
throw new Error("historical must be true or false.");
|
|
235
239
|
assertNoUnknownFlags(work, "people");
|
|
236
240
|
if (!body) {
|
|
237
241
|
console.error("Usage: frontpage people note <id> --body \"…\"");
|
|
238
242
|
return EXIT_USAGE;
|
|
239
243
|
}
|
|
240
|
-
const data = await v1Post(`${base}/${encodeURIComponent(id)}/events`, { body });
|
|
244
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/events`, { body, requestKey, ...(historical !== undefined ? { historical: historical === "true" } : {}) });
|
|
241
245
|
if (isJson()) {
|
|
242
246
|
emitOk("people", data);
|
|
243
247
|
return EXIT_OK;
|
package/dist/registry.js
CHANGED
|
@@ -407,6 +407,15 @@ export const COMMANDS = [
|
|
|
407
407
|
],
|
|
408
408
|
seeAlso: ["prompt", "help content", "help site-building"],
|
|
409
409
|
},
|
|
410
|
+
{
|
|
411
|
+
name: "customer-inbox",
|
|
412
|
+
summary: "Manage internal Customer Inbox settings, knowledge and human conversation handling",
|
|
413
|
+
usage: "frontpage customer-inbox get|put|post <resource> [--file <json>] [--request-key <key>] [--site <slug>] [--json]; export|download <id> --out <new-file>",
|
|
414
|
+
topics: ["content"],
|
|
415
|
+
examples: ["frontpage customer-inbox get settings", "frontpage customer-inbox put settings --file ./inbox-settings.json --request-key settings_revision_1", "frontpage customer-inbox get conversations", "frontpage customer-inbox get knowledge"],
|
|
416
|
+
flags: ["Internal plans and the site pilot apply", "Writes require a stable --request-key and a JSON file; reuse both on retries", "Human CLI access can guide, take over and reply; website-agent tools have management scope only", "Luna and 10× AI billing are fixed", "Full request examples: CUSTOMER-INBOX.md"],
|
|
417
|
+
seeAlso: ["people", "usage"],
|
|
418
|
+
},
|
|
410
419
|
{
|
|
411
420
|
name: "inbox",
|
|
412
421
|
summary: "List leads / subscribers / orders",
|
|
@@ -461,7 +470,7 @@ export const COMMANDS = [
|
|
|
461
470
|
"frontpage people export",
|
|
462
471
|
],
|
|
463
472
|
flags: [
|
|
464
|
-
"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;
|
|
473
|
+
"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",
|
|
465
474
|
"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.",
|
|
466
475
|
"CSV: --csv PATH plus --file mapping.json. import start requires a stable --request-key. Retry the same import with the same key and contents.",
|
|
467
476
|
"list/export: --group GROUP_ID --stage STAGE_ID. See package PEOPLE.md for full examples and every JSON shape.",
|
package/llms.txt
CHANGED
|
@@ -35,3 +35,5 @@
|
|
|
35
35
|
## People groups, stages, rules, and imports
|
|
36
36
|
|
|
37
37
|
Use `frontpage people overview`, `groups`, `stages`, `membership`, `rules`, `attachments`, `import`, and `company`. 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
|
+
|
|
39
|
+
Customer Inbox (internal sites): CUSTOMER-INBOX.md. Settings, widget, knowledge, human guidance/relay, lifecycle, export/download and recovery. Website agents configure through manage_customer_inbox and must not use human credentials for conversations.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontpage-host",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Frontpage CLI
|
|
3
|
+
"version": "0.1.29",
|
|
4
|
+
"description": "Frontpage CLI \u2014 manage your site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"frontpage": "dist/bin.js"
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"README.md",
|
|
12
12
|
"AGENTS.md",
|
|
13
13
|
"llms.txt",
|
|
14
|
-
"PEOPLE.md"
|
|
14
|
+
"PEOPLE.md",
|
|
15
|
+
"CUSTOMER-INBOX.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "tsc -p tsconfig.json",
|