@mccaskie-labs/workbench-config 0.2.5 → 0.3.1
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/CHANGELOG.md +12 -0
- package/capabilities/availability.json +263 -0
- package/capabilities/country.json +20 -1
- package/capabilities/database.json +32 -3
- package/capabilities/email-inbound.json +47 -3
- package/capabilities/email.json +66 -2
- package/capabilities/schema.json +4 -6
- package/capabilities/strings.json +46 -3
- package/dist/capabilities.gen.d.ts +381 -6
- package/dist/capabilities.gen.js +474 -10
- package/dist/capability-schema.d.ts +3 -8
- package/dist/capability-schema.js +2 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +4 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @mccaskie-labs/workbench-config
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A new component, `availability` (workbench-availability: quotes, the bookings store and the tap-to-block calendar), with its client questions (what is booked, how it is booked in a day, event types, holidays, holds). The country manifest gains holidays (`holidaysFor`, `checkHolidays`) and the December probe `country.holidays`.
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Every component now ships its client questions: email (which emails, where replies go, drafts of the wording), inbound email (which addresses, attachments), strings (other languages) and the database (what to store and who sees it). `OFFER_PENDING` is removed, and so is `delivery.when`: a component's `offer.when` says when its tool applies, and a manifest with neither `offer` nor `no_offer` fails the checks.
|
|
14
|
+
|
|
3
15
|
## 0.2.5
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./schema.json",
|
|
3
|
+
"format": 1,
|
|
4
|
+
"component": "availability",
|
|
5
|
+
"title": "Availability and pricing",
|
|
6
|
+
"summary": "workbench-availability answers \"is this date free, and what does it cost?\" for a bookable thing (a venue, a room, a photographer's day) from its calendar and its price rules, with the same answer every time and one plain line per rule applied. A store over the app's own database stops two bookings landing on one date, and a tap-to-block calendar lets the owner close dates from a phone. It runs in the app itself, with no Gateway call, no AI and no cost.",
|
|
7
|
+
"status": "built",
|
|
8
|
+
"delivery": {
|
|
9
|
+
"choice": "optional",
|
|
10
|
+
"intro": "Which dates are free and what each costs, worked out the same way every time, with a calendar the owner taps to block dates."
|
|
11
|
+
},
|
|
12
|
+
"surfaces": {
|
|
13
|
+
"package": [
|
|
14
|
+
"quote",
|
|
15
|
+
"createAvailability",
|
|
16
|
+
"availabilityMigration",
|
|
17
|
+
"calendarAction",
|
|
18
|
+
"checkSettings",
|
|
19
|
+
"stayClaims",
|
|
20
|
+
"staysClash",
|
|
21
|
+
"todayIn",
|
|
22
|
+
"AvailabilityError"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"rules": [
|
|
26
|
+
{
|
|
27
|
+
"id": "availability.quote-is-fact",
|
|
28
|
+
"kind": "must",
|
|
29
|
+
"text": "Take every availability answer and price the app shows or sends from quote() (or createAvailability's quote), and pass its lines and explain on as facts. Never let AI work out a price or a free date, and never write a price into a page or prompt by hand.",
|
|
30
|
+
"enforced_by": "review",
|
|
31
|
+
"probe": "availability.quote"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "availability.book-through-block",
|
|
35
|
+
"kind": "must",
|
|
36
|
+
"text": "Book, hold and block dates only through createAvailability's block(), which runs availability_block() in one transaction: never insert into availability_blocks directly, so the exclusion constraint and the hold expiry always apply.",
|
|
37
|
+
"enforced_by": "database",
|
|
38
|
+
"probe": "availability.store"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "availability.calendar-days",
|
|
42
|
+
"kind": "must",
|
|
43
|
+
"text": "Treat dates as calendar days (YYYY-MM-DD) in the account's time zone: pass todayIn(timeZone) as today, never new Date() in UTC.",
|
|
44
|
+
"enforced_by": "types",
|
|
45
|
+
"probe": "availability.quote"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "availability.no-names-in-notes",
|
|
49
|
+
"kind": "must_not",
|
|
50
|
+
"text": "Write a guest's name or phone number into a block's note: link the app's own booking in subject instead. The .ics feed carries neither.",
|
|
51
|
+
"enforced_by": "review",
|
|
52
|
+
"probe": "availability.store"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "availability.public-gets-the-quote",
|
|
56
|
+
"kind": "must_not",
|
|
57
|
+
"text": "Let a public page or the anonymous key read availability_blocks: public pages get the finished quote from the server.",
|
|
58
|
+
"enforced_by": "database",
|
|
59
|
+
"probe": "availability.store"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "availability.daily-cleanup",
|
|
63
|
+
"kind": "must",
|
|
64
|
+
"text": "Run createAvailability's cleanup() once a day from the app's cron with the service client, so lapsed holds show as expired and old blocks are deleted on time.",
|
|
65
|
+
"enforced_by": "review",
|
|
66
|
+
"probe": "availability.store"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "availability.calendar-as-shipped",
|
|
70
|
+
"kind": "must",
|
|
71
|
+
"text": "Draw the owner's calendar with renderCalendar and CALENDAR_SCRIPT (under the page's nonce), and hand the posted body to calendarAction behind the owner's sign-in. Never hand-write the calendar's markup or script.",
|
|
72
|
+
"enforced_by": "review",
|
|
73
|
+
"probe": "availability.calendar"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"examples": [
|
|
77
|
+
{
|
|
78
|
+
"id": "availability.example.quote",
|
|
79
|
+
"title": "Answer \"is 12 Dec free, and how much?\"",
|
|
80
|
+
"lang": "ts",
|
|
81
|
+
"probe": "availability.quote",
|
|
82
|
+
"code": "import { createAvailability } from '@mccaskie-labs/workbench-availability'\nimport settings from '../workbench/availability.json'\n\nconst av = createAvailability({ db: serviceClient, account: venue.id, settings, country: 'PH', timeZone: 'Asia/Manila' })\nconst q = await av.quote({ resource: 'villa-sol-pool', date: '2026-12-12', stay: 'day_use', guests: { min: 15, max: 15 }, eventType: 'birthday' })\n// q.available 'yes'; q.options[0].total { amount: '9500.00', currency: 'PHP' }; q.explain ['Weekend day use: ₱9,500 (rule r-weekend)', ...]"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "availability.example.calendar",
|
|
86
|
+
"title": "The owner's tap-to-block calendar",
|
|
87
|
+
"lang": "ts",
|
|
88
|
+
"probe": "availability.calendar",
|
|
89
|
+
"code": "import { calendarAction } from '@mccaskie-labs/workbench-availability'\nimport { CALENDAR_SCRIPT, renderCalendar } from '@mccaskie-labs/workbench-availability/calendar'\n\nconst days = await av.month('villa-sol-pool', '2026-12')\nconst html = renderCalendar(days, t, { resource: 'villa-sol-pool', month: '2026-12', endpoint: '/owner/calendar', stays: [{ id: 'day_use', label: t('ui.stay.day_use')! }] })\n// The page adds <script nonce=...>${CALENDAR_SCRIPT}</script>; POST /owner/calendar (owner signed in):\nconst r = await calendarAction(av, await request.json())\nreturn Response.json(r.body, { status: r.status })"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"probes": [
|
|
93
|
+
{
|
|
94
|
+
"id": "availability.quote",
|
|
95
|
+
"half": "runtime",
|
|
96
|
+
"expect": "pass",
|
|
97
|
+
"smoke": true,
|
|
98
|
+
"cost_hint_usd": 0,
|
|
99
|
+
"note": "In the Worker itself: quote() on the spec's HmPo example (Sat 12 Dec ₱9,500, Sat 19 Dec ₱10,925 with peak, 25 guests ₱10,250 over capacity, no guests a from price) and the alternatives when 19 Dec is booked; no database, no Gateway call"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "availability.store",
|
|
103
|
+
"half": "runtime",
|
|
104
|
+
"expect": "pass",
|
|
105
|
+
"cost_hint_usd": 0,
|
|
106
|
+
"note": "Against the probe app's own database: two block(booked) calls for one night at once (one lands, one refused as clash), a lapsed hold quoted free and marked expired by cleanup(), another account's resource unreachable, then every probe block removed"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "availability.calendar",
|
|
110
|
+
"half": "runtime",
|
|
111
|
+
"expect": "pass",
|
|
112
|
+
"cost_hint_usd": 0,
|
|
113
|
+
"note": "The probe page draws a month with renderCalendar and CALENDAR_SCRIPT: tap, Block day, Undo leaves no block; a change made offline shows \"1 change waiting\" and is sent on reconnect"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"styles": {
|
|
117
|
+
"prefix": "wb-cal-"
|
|
118
|
+
},
|
|
119
|
+
"offer": {
|
|
120
|
+
"pattern": "settings",
|
|
121
|
+
"when": "The client takes bookings for something with dates: a venue, a room, a resort day pass, a photographer's or hire business's days, or wants prices that depend on the day, season, holiday, number of guests or kind of event, or wants a calendar to block dates.",
|
|
122
|
+
"say": "I'll set this up with Workbench availability, so every price and free date comes out the same way each time, explained line by line, and you get a calendar to block dates from your phone. Here's how a business like yours usually books; tell me what to change.",
|
|
123
|
+
"answers_file": "workbench/availability.json",
|
|
124
|
+
"answers_format": "`{ \"format\": 1, \"status\": \"draft\" | \"done\", \"what\": \"<the client's words>\", \"stay_types\": [{ \"id\", \"start\", \"end\", \"clashes\" }], \"event_types\": [...], \"currency\": \"PHP\", \"alternatives\": { \"count\": 3, \"within_days\": 21 }, \"hold_days\": 3, \"holidays\": \"weekend\" | \"weekday\" | \"separate\" }`. Every stay type clashes with itself; list each pair that can't share a date on both sides. Take `currency` from workbench/country.json. checkSettings() reads this file at startup and names every problem. Each account's resources, packages and price rules are data in the app's database (availability_resources, availability_packages, availability_price_rules), entered on the owner's pages, never in this file. Copy availabilityMigration into supabase/migrations with its down file, and CALENDAR_WORDS into the strings catalogue.",
|
|
125
|
+
"needs": ["country"],
|
|
126
|
+
"questions": [
|
|
127
|
+
{
|
|
128
|
+
"id": "availability.what",
|
|
129
|
+
"sets": "availability.what",
|
|
130
|
+
"ask": "What do people book?",
|
|
131
|
+
"style": "table",
|
|
132
|
+
"must_ask": true,
|
|
133
|
+
"other": {
|
|
134
|
+
"kind": "text",
|
|
135
|
+
"min": 3,
|
|
136
|
+
"max": 200,
|
|
137
|
+
"description": "In the client's words, such as \"a resort's pool and function hall\" or \"me, as a photographer, by the half or full day\"."
|
|
138
|
+
},
|
|
139
|
+
"note": "Only the client knows. Their answer names the resources (one per thing booked separately) and suggests the stay types to propose next."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "availability.stays",
|
|
143
|
+
"sets": "availability.stay_types",
|
|
144
|
+
"ask": "Here are the usual ways it's booked in a day, with their hours and which can't share a date. Accept them, change some, or add or remove?",
|
|
145
|
+
"style": "table",
|
|
146
|
+
"must_ask": true,
|
|
147
|
+
"other": {
|
|
148
|
+
"kind": "list",
|
|
149
|
+
"min": 1,
|
|
150
|
+
"max": 10,
|
|
151
|
+
"description": "Each way it's booked, with its start and end time and the others it can't share a date with."
|
|
152
|
+
},
|
|
153
|
+
"note": "Propose from the client's answer to availability.what. A resort or venue: day use 08:00 to 17:00, night use 19:00 to 07:00, overnight 14:00 to 12:00 the next day (overnight clashes with both; day and night use don't clash with each other). A photographer or hire business: half day and full day, clashing with each other. One room or slot a day: a single stay type."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "availability.events",
|
|
157
|
+
"sets": "availability.event_types",
|
|
158
|
+
"ask": "Should prices depend on the kind of event? Here is the usual list.",
|
|
159
|
+
"style": "cards",
|
|
160
|
+
"answers": [
|
|
161
|
+
{
|
|
162
|
+
"label": "Yes, use this list",
|
|
163
|
+
"value": "list",
|
|
164
|
+
"means": "Birthday, debut, wedding, baptism, reunion, team building, other: a rule can add a fee for some of them."
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"label": "No, same price for all",
|
|
168
|
+
"value": "none",
|
|
169
|
+
"means": "No event types; the quote never asks for one."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"label": "Yes, with my own list",
|
|
173
|
+
"value": "own",
|
|
174
|
+
"means": "Type the kinds of event the business prices differently."
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
"other": {
|
|
178
|
+
"kind": "list",
|
|
179
|
+
"min": 1,
|
|
180
|
+
"max": 20,
|
|
181
|
+
"description": "The kinds of event, each a short name."
|
|
182
|
+
},
|
|
183
|
+
"note": "Recommend the usual list for venues and resorts; \"none\" for most other businesses. Write ids in lower case with underscores (team_building)."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "availability.holidays",
|
|
187
|
+
"sets": "availability.holidays",
|
|
188
|
+
"ask": "Should holidays be priced like weekends?",
|
|
189
|
+
"style": "cards",
|
|
190
|
+
"answers": [
|
|
191
|
+
{
|
|
192
|
+
"label": "Yes, like a weekend",
|
|
193
|
+
"value": "weekend",
|
|
194
|
+
"means": "A regular holiday or special non-working day on a weekday costs the weekend price."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"label": "No, by the day of the week",
|
|
198
|
+
"value": "weekday",
|
|
199
|
+
"means": "Holidays cost what their day of the week costs."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"label": "Set holiday prices separately",
|
|
203
|
+
"value": "separate",
|
|
204
|
+
"means": "Holidays get their own price rules, entered on the owner's pages."
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"other": {
|
|
208
|
+
"kind": "text",
|
|
209
|
+
"min": 3,
|
|
210
|
+
"max": 200,
|
|
211
|
+
"description": "How holidays should be priced, in the client's words."
|
|
212
|
+
},
|
|
213
|
+
"note": "Holidays come from workbench-country's yearly list (the Philippines' proclamation each year); a local fiesta goes in the availability_holidays table. Special working days (the EDSA anniversary) are never priced as holidays."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "availability.holds",
|
|
217
|
+
"sets": "availability.hold_days",
|
|
218
|
+
"ask": "How long is a date held while waiting for a deposit?",
|
|
219
|
+
"style": "cards",
|
|
220
|
+
"answers": [
|
|
221
|
+
{
|
|
222
|
+
"label": "3 days",
|
|
223
|
+
"value": 3,
|
|
224
|
+
"means": "The date shows as held (H) for 3 days, then is free again unless marked booked."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"label": "1 day",
|
|
228
|
+
"value": 1
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"label": "7 days",
|
|
232
|
+
"value": 7
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"label": "No holds",
|
|
236
|
+
"value": 0,
|
|
237
|
+
"means": "A date is free until it is booked; nothing is held."
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"other": {
|
|
241
|
+
"kind": "whole",
|
|
242
|
+
"min": 0,
|
|
243
|
+
"max": 60,
|
|
244
|
+
"description": "Days, 0 to 60."
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
"not_asked": [
|
|
249
|
+
{
|
|
250
|
+
"sets": "availability.prices",
|
|
251
|
+
"why": "Each account's prices, packages and rules change often, so they are data the owner enters on their own pages (or that onboarding reads from their rate card), not project settings."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"sets": "availability.alternatives",
|
|
255
|
+
"why": "Up to 3 nearest free dates within 21 days, same weekday first, suits every business so far; change it in the answers file if a client asks."
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"sets": "availability.currency",
|
|
259
|
+
"why": "It is the country's own, from workbench/country.json."
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"currencySymbol",
|
|
26
26
|
"currencySymbolFrom",
|
|
27
27
|
"WORLDWIDE",
|
|
28
|
-
"isWorldwide"
|
|
28
|
+
"isWorldwide",
|
|
29
|
+
"HOLIDAYS",
|
|
30
|
+
"holidaysFor",
|
|
31
|
+
"holidayOn",
|
|
32
|
+
"checkHolidays"
|
|
29
33
|
]
|
|
30
34
|
},
|
|
31
35
|
"rules": [
|
|
@@ -50,6 +54,13 @@
|
|
|
50
54
|
"enforced_by": "types",
|
|
51
55
|
"probe": "country.packs"
|
|
52
56
|
},
|
|
57
|
+
{
|
|
58
|
+
"id": "country.holidays-from-proclamation",
|
|
59
|
+
"kind": "must",
|
|
60
|
+
"text": "Take public holidays from holidaysFor or holidayOn, never worked out or guessed: a year with no list returns undefined, and checkHolidays fails from 1 December until next year's list is in.",
|
|
61
|
+
"enforced_by": "review",
|
|
62
|
+
"probe": "country.holidays"
|
|
63
|
+
},
|
|
53
64
|
{
|
|
54
65
|
"id": "country.spelling-from-pack",
|
|
55
66
|
"kind": "must",
|
|
@@ -82,6 +93,14 @@
|
|
|
82
93
|
"expect": "pass",
|
|
83
94
|
"cost_hint_usd": 0,
|
|
84
95
|
"note": "Every pack's spelling is one the Gateway accepts (en-PH, en-AU, en-GB, en-US), checked against the ai manifest's spellings; no Gateway call"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "country.holidays",
|
|
99
|
+
"half": "runtime",
|
|
100
|
+
"expect": "pass",
|
|
101
|
+
"smoke": true,
|
|
102
|
+
"cost_hint_usd": 0,
|
|
103
|
+
"note": "The December probe: checkHolidays for every country with a built-in list, on today in that country's time zone. Fails when this year's list is missing, and from 1 December when next year's is, so the list is added from the proclamation before January's prices need it"
|
|
85
104
|
}
|
|
86
105
|
],
|
|
87
106
|
"offer": {
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"status": "built",
|
|
8
8
|
"delivery": {
|
|
9
9
|
"choice": "built_in",
|
|
10
|
-
"intro": "The app's own database, one for testing and one for the live site."
|
|
11
|
-
"when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration."
|
|
10
|
+
"intro": "The app's own database, one for testing and one for the live site."
|
|
12
11
|
},
|
|
13
12
|
"surfaces": {
|
|
14
13
|
"package": [
|
|
@@ -237,5 +236,35 @@
|
|
|
237
236
|
"cost_hint_usd": 0,
|
|
238
237
|
"note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
|
|
239
238
|
}
|
|
240
|
-
]
|
|
239
|
+
],
|
|
240
|
+
"offer": {
|
|
241
|
+
"pattern": "settings",
|
|
242
|
+
"when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.",
|
|
243
|
+
"say": "The app's data goes in its own Workbench database, one for testing and one for the live site. I'll propose what to store and who can see it, and change it with a migration you can undo.",
|
|
244
|
+
"answers_file": "workbench/database.json",
|
|
245
|
+
"answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"tables\": [{ \"name\": \"orders\", \"row\": \"one order\", \"columns\": [{ \"name\": \"total\", \"kind\": \"money\", \"required\": true }], \"read\": \"own\", \"change\": \"own\" }] }`. Then write one migration (`supabase/migrations/<14 digits>_<name>.sql`) with its down file, turning on row-level security with one policy per answer, apply it on staging with Workbench Control (`sb_migration_apply`), and refresh `supabase/staging-columns.json`.",
|
|
246
|
+
"questions": [
|
|
247
|
+
{
|
|
248
|
+
"id": "database.tables",
|
|
249
|
+
"sets": "tables",
|
|
250
|
+
"ask": "What should the app store, and who can see and change it?",
|
|
251
|
+
"style": "table",
|
|
252
|
+
"note": "Propose the tables the request needs, each with what one row is, its columns (name, kind such as text, number, money, date, yes/no, a link to another table; and whether it is required), and who may read and who may change its rows: `own` (the person who made the row), `admins`, `signed_in` (anyone signed in) or `public` (anyone, read only). Recommend `own` for anything about a person. Show money as whole cents, never a fraction. The client accepts, removes or adds tables and columns."
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"not_asked": [
|
|
256
|
+
{
|
|
257
|
+
"sets": "keys",
|
|
258
|
+
"why": "The app reaches the database as the signed-in person (`dbAs`), so row-level security decides what each person sees; the template holds no secret key."
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"sets": "hosting",
|
|
262
|
+
"why": "Each project has its own staging and production databases, made by Workbench Control in the region set for the install."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"sets": "sign_in_settings",
|
|
266
|
+
"why": "The database's sign-in settings follow the Workbench baseline (the workbench-sign-in skill)."
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
241
270
|
}
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"status": "built",
|
|
8
8
|
"delivery": {
|
|
9
9
|
"choice": "optional",
|
|
10
|
-
"intro": "Receiving email at the project's own addresses, for the app to act on."
|
|
11
|
-
"when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles."
|
|
10
|
+
"intro": "Receiving email at the project's own addresses, for the app to act on."
|
|
12
11
|
},
|
|
13
12
|
"surfaces": {
|
|
14
13
|
"tools": [
|
|
@@ -129,5 +128,50 @@
|
|
|
129
128
|
"two_step": true,
|
|
130
129
|
"note": "cf_email_routing_rule_delete returns a confirmation and changes nothing until Robert approves"
|
|
131
130
|
}
|
|
132
|
-
]
|
|
131
|
+
],
|
|
132
|
+
"offer": {
|
|
133
|
+
"pattern": "switch_on",
|
|
134
|
+
"when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.",
|
|
135
|
+
"say": "The project can already receive email with Workbench inbound email: each address goes straight to the app, which decides what to do with every message. I'll switch on the addresses you want.",
|
|
136
|
+
"answers_file": "workbench/email-inbound.json",
|
|
137
|
+
"answers_format": "`{ \"format\": 1, \"addresses\": [{ \"name\": \"hello\", \"does\": \"saves it as a message the admins see\" }] }`. Then set `INBOUND_EMAIL` to `\"on\"` in `wrangler.jsonc`, write `handleEmail` in `src/app.ts` to do what each address `does`, and make the addresses with Workbench Control: `cf_email_routing_enable`, then `cf_email_routing_rule_create` for each address on staging first (the capabilities guide below has the steps).",
|
|
138
|
+
"questions": [
|
|
139
|
+
{
|
|
140
|
+
"id": "email-inbound.addresses",
|
|
141
|
+
"sets": "addresses",
|
|
142
|
+
"ask": "Which addresses should the app receive, and what should it do with each message?",
|
|
143
|
+
"style": "table",
|
|
144
|
+
"note": "Propose the addresses the request needs, each with its name (the part before @, lower case: `hello`, `bookings`) and what the app does with a message, in the client's words: save it for the admins to read, add it to a booking, answer it automatically. One address, one job. Say plainly that the app reads the mail itself: nothing is forwarded to anyone's inbox."
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "email-inbound.attachments",
|
|
148
|
+
"sets": "attachments",
|
|
149
|
+
"ask": "What should happen to files attached to these emails?",
|
|
150
|
+
"style": "cards",
|
|
151
|
+
"answers": [
|
|
152
|
+
{
|
|
153
|
+
"label": "Leave them out",
|
|
154
|
+
"value": "drop",
|
|
155
|
+
"means": "The app keeps the words of each email, not its files. Nothing extra to store or check."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"label": "Keep them",
|
|
159
|
+
"value": "keep",
|
|
160
|
+
"means": "The app stores each file for the admins. Files can be large or unsafe, so keep only the types the job needs."
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"note": "With keep, ask which types (photos, PDFs) and store them outside the Worker, in file storage the app already has, or ask the owner before adding some; refuse other types with `reject`."
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"not_asked": [
|
|
167
|
+
{
|
|
168
|
+
"sets": "domain",
|
|
169
|
+
"why": "Addresses are on the project's own email domain: its staging name first, and the client's own domain once it is attached for production (`cf_zone_attach`)."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"sets": "forwarding",
|
|
173
|
+
"why": "Workbench never forwards mail outside the project or catches every address: each literal address goes to the app."
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
133
177
|
}
|
package/capabilities/email.json
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"status": "built",
|
|
8
8
|
"delivery": {
|
|
9
9
|
"choice": "built_in",
|
|
10
|
-
"intro": "The emails the app sends, such as confirmations, receipts and sign-in codes."
|
|
11
|
-
"when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code."
|
|
10
|
+
"intro": "The emails the app sends, such as confirmations, receipts and sign-in codes."
|
|
12
11
|
},
|
|
13
12
|
"surfaces": {
|
|
14
13
|
"gateway": ["POST /v1/email/send"],
|
|
@@ -71,5 +70,70 @@
|
|
|
71
70
|
],
|
|
72
71
|
"styles": {
|
|
73
72
|
"prefix": "wb-email-"
|
|
73
|
+
},
|
|
74
|
+
"offer": {
|
|
75
|
+
"pattern": "settings",
|
|
76
|
+
"when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.",
|
|
77
|
+
"say": "I'll send these with Workbench email, which sends from the app's own address in your brand, with no email key in the app. The sign-in codes are already there; I'll add yours beside them.",
|
|
78
|
+
"answers_file": "workbench/email.json",
|
|
79
|
+
"answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"reply_to\": null, \"emails\": [{ \"id\": \"order_confirmed\", \"when\": \"a customer sends an order\", \"to\": \"the customer\", \"parts\": [\"heading\", \"paragraph\", \"button\"], \"link_changes_something\": false }] }`. Declare each email in `src/lib/emails.ts` with `defineEmails` (stream `transactional`, shell `account`, `decidedBy` naming this answers file), put its words in the strings under `email.<id>.<key>`, and send it with `mailerFor(env)` and an `idempotencyKey` whenever a retry could repeat it.",
|
|
80
|
+
"questions": [
|
|
81
|
+
{
|
|
82
|
+
"id": "email.emails",
|
|
83
|
+
"sets": "emails",
|
|
84
|
+
"ask": "Which emails should the app send?",
|
|
85
|
+
"style": "table",
|
|
86
|
+
"note": "Propose the emails the request needs, each with an id (snake_case: `order_confirmed`), when it is sent, who gets it, its parts in order (heading, paragraphs, a button and its link, small print) and whether its link changes something (then the link lands on a confirmation page first). Only emails a person's own action or account causes: receipts, confirmations, notices. The client accepts, removes or adds. The wording is drafted afterwards, one email at a time."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "email.reply-to",
|
|
90
|
+
"sets": "reply_to",
|
|
91
|
+
"ask": "Where should a reply to these emails go?",
|
|
92
|
+
"style": "cards",
|
|
93
|
+
"answers": [
|
|
94
|
+
{
|
|
95
|
+
"label": "Nowhere",
|
|
96
|
+
"value": null,
|
|
97
|
+
"means": "The emails come from an address that isn't read. Say so in the small print, with where to get help instead."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"label": "To the app",
|
|
101
|
+
"value": "app",
|
|
102
|
+
"means": "Replies go to an address the app reads itself (Workbench inbound email)."
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"other": {
|
|
106
|
+
"kind": "text",
|
|
107
|
+
"min": 6,
|
|
108
|
+
"max": 254,
|
|
109
|
+
"description": "An email address someone at the business reads."
|
|
110
|
+
},
|
|
111
|
+
"must_ask": true,
|
|
112
|
+
"note": "A typed address goes in each send as `replyTo`; check it looks like an email address. With the app, the workbench-email-inbound skill sets up the address."
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"not_asked": [
|
|
116
|
+
{
|
|
117
|
+
"sets": "sender",
|
|
118
|
+
"why": "Emails come from the project's own sending address (`FROM` in `src/lib/email.ts`) on its own domain, which Workbench Control sets up."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"sets": "marketing",
|
|
122
|
+
"why": "Only transactional email exists: newsletters and other bulk mail are refused until Workbench has a sender for them, so offer none."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"sets": "look",
|
|
126
|
+
"why": "Emails take the brand's name, colour and logo, and the project's `project.css` (see Restyling)."
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"drafts": {
|
|
130
|
+
"of": "wording for one email",
|
|
131
|
+
"surface": "ai",
|
|
132
|
+
"alias": "claude-default",
|
|
133
|
+
"count": 3,
|
|
134
|
+
"max_rounds": 3,
|
|
135
|
+
"job": "ai.drafts",
|
|
136
|
+
"command": "npx workbench-ai drafts text email_<id> --about \"<what the email is for, who gets it and its parts>\""
|
|
137
|
+
}
|
|
74
138
|
}
|
|
75
139
|
}
|
package/capabilities/schema.json
CHANGED
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"strings",
|
|
36
36
|
"ui",
|
|
37
37
|
"forms",
|
|
38
|
-
"sign-in"
|
|
38
|
+
"sign-in",
|
|
39
|
+
"availability"
|
|
39
40
|
]
|
|
40
41
|
},
|
|
41
42
|
"title": {
|
|
@@ -430,7 +431,8 @@
|
|
|
430
431
|
"strings",
|
|
431
432
|
"ui",
|
|
432
433
|
"forms",
|
|
433
|
-
"sign-in"
|
|
434
|
+
"sign-in",
|
|
435
|
+
"availability"
|
|
434
436
|
]
|
|
435
437
|
}
|
|
436
438
|
},
|
|
@@ -745,10 +747,6 @@
|
|
|
745
747
|
"type": "string",
|
|
746
748
|
"minLength": 1,
|
|
747
749
|
"maxLength": 160
|
|
748
|
-
},
|
|
749
|
-
"when": {
|
|
750
|
-
"type": "string",
|
|
751
|
-
"minLength": 1
|
|
752
750
|
}
|
|
753
751
|
},
|
|
754
752
|
"required": [
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
"status": "built",
|
|
8
8
|
"delivery": {
|
|
9
9
|
"choice": "built_in",
|
|
10
|
-
"intro": "The words on the app's screens and in its emails, kept in one place for each language."
|
|
11
|
-
"when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language."
|
|
10
|
+
"intro": "The words on the app's screens and in its emails, kept in one place for each language."
|
|
12
11
|
},
|
|
13
12
|
"surfaces": {
|
|
14
13
|
"package": [
|
|
@@ -132,5 +131,49 @@
|
|
|
132
131
|
"cost_hint_usd": 0,
|
|
133
132
|
"note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
|
|
134
133
|
}
|
|
135
|
-
]
|
|
134
|
+
],
|
|
135
|
+
"offer": {
|
|
136
|
+
"pattern": "project",
|
|
137
|
+
"when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.",
|
|
138
|
+
"say": "Every word the app shows is kept in one place with Workbench strings, so changing wording never means hunting through code, and other languages can be added later.",
|
|
139
|
+
"answers_file": "workbench/strings.json",
|
|
140
|
+
"answers_format": "`{ \"format\": 1, \"languages\": [\"en-AU\"], \"later\": [\"fil\"] }`: the language the site is written in first, and any the client wants later. New wording goes into `strings/strings.<base>.json` with one key per sentence, and `npm run check:strings` checks it.",
|
|
141
|
+
"questions": [
|
|
142
|
+
{
|
|
143
|
+
"id": "strings.languages",
|
|
144
|
+
"sets": "languages",
|
|
145
|
+
"ask": "Does the site need to be in any language besides English?",
|
|
146
|
+
"style": "cards",
|
|
147
|
+
"answers": [
|
|
148
|
+
{
|
|
149
|
+
"label": "English only",
|
|
150
|
+
"value": "none",
|
|
151
|
+
"means": "Every page and email is in English."
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"label": "Another language later",
|
|
155
|
+
"value": "later",
|
|
156
|
+
"means": "English now. The words are kept so a translation can be added, checked by a native speaker, without changing the code."
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
"other": {
|
|
160
|
+
"kind": "text",
|
|
161
|
+
"min": 2,
|
|
162
|
+
"max": 120,
|
|
163
|
+
"description": "The languages, such as Filipino or Spanish."
|
|
164
|
+
},
|
|
165
|
+
"note": "Translations come with a Workbench tool that isn't built yet: record the languages under `later`, keep every word in the catalogue, and tell the client a translation goes live only once a named native speaker has checked it."
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"not_asked": [
|
|
169
|
+
{
|
|
170
|
+
"sets": "base",
|
|
171
|
+
"why": "The site is written first in English, in the readers' spelling from the country answers, so it isn't asked again."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"sets": "wording",
|
|
175
|
+
"why": "Wording is content, not a setting: the client says what they want in their own words, or asks for drafts (the workbench-ai skill), and it goes into the catalogue."
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
136
179
|
}
|