@mccaskie-labs/workbench-config 0.3.0 → 0.3.2
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/ai.json +212 -7
- package/capabilities/availability.json +263 -0
- package/capabilities/country.json +20 -1
- package/capabilities/schema.json +4 -2
- package/dist/capabilities.gen.d.ts +409 -7
- package/dist/capabilities.gen.js +511 -7
- package/dist/capability-schema.d.ts +3 -1
- package/dist/capability-schema.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MANIFEST_VERSION: "
|
|
1
|
+
export declare const MANIFEST_VERSION: "4a52a6ba9520";
|
|
2
2
|
export declare const capabilities: {
|
|
3
3
|
readonly ai: {
|
|
4
4
|
readonly $schema: "./schema.json";
|
|
@@ -9,7 +9,7 @@ export declare const capabilities: {
|
|
|
9
9
|
readonly status: "built";
|
|
10
10
|
readonly surfaces: {
|
|
11
11
|
readonly gateway: readonly ["POST /anthropic/v1/messages", "POST /anthropic/v1/messages/count_tokens"];
|
|
12
|
-
readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi", "checkWriting", "writingStops", "writingNotes", "writingFixNote"];
|
|
12
|
+
readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.countTokens", "Ai.budget", "createFakeAi", "checkWriting", "writingStops", "writingNotes", "writingFixNote", "Ai.reply", "checkGuide"];
|
|
13
13
|
};
|
|
14
14
|
readonly config: readonly [{
|
|
15
15
|
readonly name: "WORKBENCH_GATEWAY_KEY";
|
|
@@ -114,6 +114,12 @@ export declare const capabilities: {
|
|
|
114
114
|
readonly text: "Name a Workbench voice and spelling with the voice and spelling options, so the Gateway adds the text, or bring the app's own voice. Never paste Workbench's voice text into a prompt.";
|
|
115
115
|
readonly enforced_by: "gateway";
|
|
116
116
|
readonly probe: "ai.voice";
|
|
117
|
+
}, {
|
|
118
|
+
readonly id: "ai.reply-facts-from-code";
|
|
119
|
+
readonly kind: "must";
|
|
120
|
+
readonly text: "Draft replies with ai.reply from facts worked out by code (prices and dates from quote(), never from the AI), and show the draft in renderReplyBox for a person to check and copy. Never send a draft automatically, and never let a draft with needs_human_check go out unread.";
|
|
121
|
+
readonly enforced_by: "review";
|
|
122
|
+
readonly probe: "ai.reply";
|
|
117
123
|
}];
|
|
118
124
|
readonly examples: readonly [{
|
|
119
125
|
readonly id: "ai.example.text";
|
|
@@ -127,6 +133,12 @@ export declare const capabilities: {
|
|
|
127
133
|
readonly lang: "ts";
|
|
128
134
|
readonly code: "const r = await ai.text({ model: 'claude-default', job: 'article.write', voice: 'conversational', spelling: 'en-AU', prompt })\nconst issues = checkWriting(r.text, { voice: 'conversational' })\nif (writingStops(issues).length > 0) await retryWith(prompt + writingFixNote(issues))";
|
|
129
135
|
readonly probe: "ai.voice";
|
|
136
|
+
}, {
|
|
137
|
+
readonly id: "ai.example.reply";
|
|
138
|
+
readonly title: "Draft a reply to an enquiry";
|
|
139
|
+
readonly lang: "ts";
|
|
140
|
+
readonly code: "import { REPLY_GUIDES } from './replies.generated'\nconst ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env), url: env.WORKBENCH_GATEWAY_URL, replyGuides: REPLY_GUIDES })\nconst q = quote(settings, ask)\nconst draft = await ai.reply({\n guide: 'venue-quote',\n facts: { venue_name: venue.name, date: ask.date, available: q.available, packages: q.options.map((o) => ({ name: o.package, price: o.total.amount })), alternatives: q.alternatives.map((a) => a.date) },\n incoming: { text, intake: read },\n language: account.reply_language,\n})\nreturn html(renderReplyBox(draft, { againAction: `/enquiries/${id}/reply` }))";
|
|
141
|
+
readonly probe: "ai.reply";
|
|
130
142
|
}];
|
|
131
143
|
readonly probes: readonly [{
|
|
132
144
|
readonly id: "ai.text.claude-default";
|
|
@@ -193,18 +205,23 @@ export declare const capabilities: {
|
|
|
193
205
|
readonly half: "runtime";
|
|
194
206
|
readonly expect: "pass";
|
|
195
207
|
readonly cost_hint_usd: 0.008;
|
|
208
|
+
}, {
|
|
209
|
+
readonly id: "ai.reply";
|
|
210
|
+
readonly half: "runtime";
|
|
211
|
+
readonly expect: "pass";
|
|
212
|
+
readonly cost_hint_usd: 0.005;
|
|
196
213
|
}];
|
|
197
214
|
readonly offer: {
|
|
198
215
|
readonly pattern: "feature_or_content";
|
|
199
|
-
readonly when: "The client wants the app to write, summarise, sort, answer questions or pull details out of text with AI, as a feature visitors use, or wants wording for the site written once.";
|
|
216
|
+
readonly when: "The client wants the app to write, summarise, sort, answer questions or pull details out of text with AI, as a feature visitors use, or wants wording for the site written once, or wants ready-to-paste replies to the messages the business gets (quote requests, booking questions).";
|
|
200
217
|
readonly say: "I'll do this with Workbench AI, which uses Claude through Workbench, so the app holds no AI key and spending has a monthly cap.";
|
|
201
218
|
readonly answers_file: "workbench/ai/<name>.json";
|
|
202
219
|
readonly per: "feature or piece of wording";
|
|
203
|
-
readonly answers_format: "For a feature: `{ \"format\": 1, \"route\": \"feature\", \"name\", \"about\" (the client's words), \"who\", \"cap_usd\", \"keep\", \"job\" }`, where `job` is the `X-Workbench-Job` the feature's calls send (`<name>` with dots, such as `quotes.estimate`), so its spending shows on its own line in Control. Write it, then build the feature with the package. For wording made once: `npx workbench-ai pick <name> <round>-<draft>` writes it from the draft the client picked, with the round's request ids and cost; never write one from nothing. Then put the wording into the strings catalogue (the workbench-strings skill), keyed for where it is shown. Drafts and their previews stay in `workbench/ai/drafts/`, which isn't committed.";
|
|
220
|
+
readonly answers_format: "For a feature: `{ \"format\": 1, \"route\": \"feature\", \"name\", \"about\" (the client's words), \"who\", \"cap_usd\", \"keep\", \"job\" }`, where `job` is the `X-Workbench-Job` the feature's calls send (`<name>` with dots, such as `quotes.estimate`), so its spending shows on its own line in Control. Write it, then build the feature with the package. For wording made once: `npx workbench-ai pick <name> <round>-<draft>` writes it from the draft the client picked, with the round's request ids and cost; never write one from nothing. Then put the wording into the strings catalogue (the workbench-strings skill), keyed for where it is shown. Drafts and their previews stay in `workbench/ai/drafts/`, which isn't committed. For replies: write the guide itself to `workbench/replies/<name>.json` (format 1: `name`, `version`, `voice`, `spelling`, `max_words`, `max_questions`, `facts` as a small JSON Schema with `x-money` on amounts and `format: date` on dates, `ask_order`, `guide` lines, `examples`, `never`), then run `npx workbench-ai replies generate`, which checks it and writes `src/replies.generated.ts`; never edit that file. Offer samples with `npx workbench-ai replies drafts <name> --languages <the client's languages>` (two to four, with the development key; tell the client the cost it prints) and keep the one the client picks with `npx workbench-ai replies pick <name> <round>-<draft>`, which makes it the guide's first example.";
|
|
204
221
|
readonly questions: readonly [{
|
|
205
222
|
readonly id: "ai.route";
|
|
206
223
|
readonly sets: "route";
|
|
207
|
-
readonly ask: "Is this something visitors use on the site,
|
|
224
|
+
readonly ask: "Is this something visitors use on the site, something made once for the site, or replies to the messages you get?";
|
|
208
225
|
readonly style: "cards";
|
|
209
226
|
readonly answers: readonly [{
|
|
210
227
|
readonly label: "Visitors use it";
|
|
@@ -214,9 +231,13 @@ export declare const capabilities: {
|
|
|
214
231
|
readonly label: "Made once for the site";
|
|
215
232
|
readonly value: "content";
|
|
216
233
|
readonly means: "Wording for a page, written once and kept: an About page, a welcome, product descriptions.";
|
|
234
|
+
}, {
|
|
235
|
+
readonly label: "Replies to messages";
|
|
236
|
+
readonly value: "reply";
|
|
237
|
+
readonly means: "A ready-to-paste reply to a customer's message, with your prices and dates filled in from your own details. A person checks it and copies it; nothing is sent by itself.";
|
|
217
238
|
}];
|
|
218
239
|
readonly must_ask: true;
|
|
219
|
-
readonly note: "Ask it even when the client's words seem to say, as a quick check: the two routes are built and paid for differently.";
|
|
240
|
+
readonly note: "Ask it even when the client's words seem to say, as a quick check: the two routes are built and paid for differently. Replies are their own route: the facts (prices, dates) come from the app's own code, so ask where those facts come from before anything else.";
|
|
220
241
|
}, {
|
|
221
242
|
readonly id: "ai.who";
|
|
222
243
|
readonly sets: "feature.who";
|
|
@@ -319,6 +340,156 @@ export declare const capabilities: {
|
|
|
319
340
|
readonly is: readonly ["content"];
|
|
320
341
|
};
|
|
321
342
|
readonly note: "Name the piece after where it goes (about_us, home_blurb): lower case and underscores. The client's own words, with any facts they give (when they started, where they are), go in --about, because the drafts never invent facts and leave [gaps] for the owner instead.";
|
|
343
|
+
}, {
|
|
344
|
+
readonly id: "ai.reply.purpose";
|
|
345
|
+
readonly sets: "reply.purpose";
|
|
346
|
+
readonly ask: "What are these replies answering?";
|
|
347
|
+
readonly style: "cards";
|
|
348
|
+
readonly answers: readonly [{
|
|
349
|
+
readonly label: "Quote and price requests";
|
|
350
|
+
readonly value: "quote";
|
|
351
|
+
readonly means: "\"How much for Dec 12, 15 pax?\": the reply gives the price and asks only for what's missing.";
|
|
352
|
+
}, {
|
|
353
|
+
readonly label: "Availability questions";
|
|
354
|
+
readonly value: "availability";
|
|
355
|
+
readonly means: "\"Available pa po ba sa Sabado?\": the reply says yes or offers other dates.";
|
|
356
|
+
}, {
|
|
357
|
+
readonly label: "Booking or appointment requests";
|
|
358
|
+
readonly value: "booking";
|
|
359
|
+
readonly means: "\"May slot po ba bukas?\": the reply lists the free times and asks which one.";
|
|
360
|
+
}];
|
|
361
|
+
readonly other: {
|
|
362
|
+
readonly kind: "text";
|
|
363
|
+
readonly min: 3;
|
|
364
|
+
readonly max: 200;
|
|
365
|
+
readonly description: "What the messages are, in a few words.";
|
|
366
|
+
};
|
|
367
|
+
readonly must_ask: true;
|
|
368
|
+
readonly when: {
|
|
369
|
+
readonly id: "ai.route";
|
|
370
|
+
readonly is: readonly ["reply"];
|
|
371
|
+
};
|
|
372
|
+
readonly note: "Name the guide after it (venue-quote, appointment-slots): lower case and dashes. Say that prices and dates always come from the app's own details (workbench-availability's quote() where it's a booking), never from the AI.";
|
|
373
|
+
}, {
|
|
374
|
+
readonly id: "ai.reply.facts";
|
|
375
|
+
readonly sets: "reply.facts";
|
|
376
|
+
readonly ask: "Here are the details a reply can use. Keep, change or add?";
|
|
377
|
+
readonly style: "table";
|
|
378
|
+
readonly must_ask: true;
|
|
379
|
+
readonly when: {
|
|
380
|
+
readonly id: "ai.route";
|
|
381
|
+
readonly is: readonly ["reply"];
|
|
382
|
+
};
|
|
383
|
+
readonly note: "Propose one row per fact from the client's words and what the app already holds: its name, what it is, where the app gets it, and whether it is money (x-money with the currency) or a date. Only these can appear in a reply; a fact the app doesn't have can't be quoted. Then ask which details a reply may ask the customer for, most important first (ask_order); intake's missing fields are asked from that list only.";
|
|
384
|
+
}, {
|
|
385
|
+
readonly id: "ai.reply.language";
|
|
386
|
+
readonly sets: "reply.language";
|
|
387
|
+
readonly ask: "Which language should replies use?";
|
|
388
|
+
readonly style: "cards";
|
|
389
|
+
readonly answers: readonly [{
|
|
390
|
+
readonly label: "Match the message";
|
|
391
|
+
readonly value: "match";
|
|
392
|
+
readonly means: "English gets English, Taglish gets Taglish, Tagalog gets Tagalog. When it can't tell, it uses your fallback and asks you to check.";
|
|
393
|
+
}, {
|
|
394
|
+
readonly label: "Always Taglish";
|
|
395
|
+
readonly value: "taglish";
|
|
396
|
+
}, {
|
|
397
|
+
readonly label: "Always English";
|
|
398
|
+
readonly value: "en";
|
|
399
|
+
}, {
|
|
400
|
+
readonly label: "Always Tagalog";
|
|
401
|
+
readonly value: "tl";
|
|
402
|
+
}];
|
|
403
|
+
readonly when: {
|
|
404
|
+
readonly id: "ai.route";
|
|
405
|
+
readonly is: readonly ["reply"];
|
|
406
|
+
};
|
|
407
|
+
readonly note: "This is the default. Where the app has accounts (a venue each), make it each account's own setting in its portal, defaulting to this, and pass it to ai.reply as language; with match, pass the account's fallback as fallbackLanguage.";
|
|
408
|
+
}, {
|
|
409
|
+
readonly id: "ai.reply.voice";
|
|
410
|
+
readonly sets: "reply.voice";
|
|
411
|
+
readonly ask: "Which tone?";
|
|
412
|
+
readonly style: "cards";
|
|
413
|
+
readonly answers: readonly [{
|
|
414
|
+
readonly label: "Conversational";
|
|
415
|
+
readonly value: "conversational";
|
|
416
|
+
readonly means: "Warm and natural, like a helpful person at the front desk.";
|
|
417
|
+
}, {
|
|
418
|
+
readonly label: "Casual";
|
|
419
|
+
readonly value: "casual";
|
|
420
|
+
readonly means: "Relaxed and friendly, like chatting with a regular.";
|
|
421
|
+
}, {
|
|
422
|
+
readonly label: "Plain";
|
|
423
|
+
readonly value: "plain";
|
|
424
|
+
readonly means: "Short and clear, nothing extra.";
|
|
425
|
+
}, {
|
|
426
|
+
readonly label: "Formal";
|
|
427
|
+
readonly value: "formal";
|
|
428
|
+
readonly means: "Polite and proper, for a business that keeps a formal tone.";
|
|
429
|
+
}];
|
|
430
|
+
readonly when: {
|
|
431
|
+
readonly id: "ai.route";
|
|
432
|
+
readonly is: readonly ["reply"];
|
|
433
|
+
};
|
|
434
|
+
}, {
|
|
435
|
+
readonly id: "ai.reply.length";
|
|
436
|
+
readonly sets: "reply.max_words";
|
|
437
|
+
readonly ask: "How long can a reply be, at most?";
|
|
438
|
+
readonly style: "cards";
|
|
439
|
+
readonly answers: readonly [{
|
|
440
|
+
readonly label: "About 90 words";
|
|
441
|
+
readonly value: 90;
|
|
442
|
+
readonly means: "Room for a price or two and one question.";
|
|
443
|
+
}, {
|
|
444
|
+
readonly label: "About 50 words";
|
|
445
|
+
readonly value: 50;
|
|
446
|
+
readonly means: "A quick chat reply.";
|
|
447
|
+
}, {
|
|
448
|
+
readonly label: "About 150 words";
|
|
449
|
+
readonly value: 150;
|
|
450
|
+
readonly means: "Room for several packages or options.";
|
|
451
|
+
}];
|
|
452
|
+
readonly other: {
|
|
453
|
+
readonly kind: "whole";
|
|
454
|
+
readonly min: 20;
|
|
455
|
+
readonly max: 300;
|
|
456
|
+
readonly description: "Words, from 20 to 300.";
|
|
457
|
+
};
|
|
458
|
+
readonly when: {
|
|
459
|
+
readonly id: "ai.route";
|
|
460
|
+
readonly is: readonly ["reply"];
|
|
461
|
+
};
|
|
462
|
+
readonly note: "A reply up to a quarter over passes quietly; one over twice as long is sent back to be written again, then flagged for a person to check.";
|
|
463
|
+
}, {
|
|
464
|
+
readonly id: "ai.reply.never";
|
|
465
|
+
readonly sets: "reply.never";
|
|
466
|
+
readonly ask: "Is there anything a reply must never say?";
|
|
467
|
+
readonly style: "cards";
|
|
468
|
+
readonly answers: readonly [{
|
|
469
|
+
readonly label: "The usual three";
|
|
470
|
+
readonly value: "usual";
|
|
471
|
+
readonly means: "Never promise a discount, confirm a booking, or ask for payment details. Code checks these as well as telling the AI.";
|
|
472
|
+
}, {
|
|
473
|
+
readonly label: "The usual three, and more";
|
|
474
|
+
readonly value: "usual_and_more";
|
|
475
|
+
readonly means: "Those three plus things of your own (type them in Other).";
|
|
476
|
+
}, {
|
|
477
|
+
readonly label: "Nothing";
|
|
478
|
+
readonly value: "none";
|
|
479
|
+
readonly means: "No rules beyond Workbench's own content rules.";
|
|
480
|
+
}];
|
|
481
|
+
readonly other: {
|
|
482
|
+
readonly kind: "list";
|
|
483
|
+
readonly min: 1;
|
|
484
|
+
readonly max: 10;
|
|
485
|
+
readonly description: "Each thing a reply must never do, in a few words (give medical advice, mention the pool).";
|
|
486
|
+
};
|
|
487
|
+
readonly must_ask: true;
|
|
488
|
+
readonly when: {
|
|
489
|
+
readonly id: "ai.route";
|
|
490
|
+
readonly is: readonly ["reply"];
|
|
491
|
+
};
|
|
492
|
+
readonly note: "Write each as a never line in the guide. Lines about payment details, discounts and confirming bookings are also checked by code; others go into the prompt only, so say so if the client's line matters a lot.";
|
|
322
493
|
}];
|
|
323
494
|
readonly not_asked: readonly [{
|
|
324
495
|
readonly sets: "model";
|
|
@@ -337,6 +508,9 @@ export declare const capabilities: {
|
|
|
337
508
|
readonly command: "npx workbench-ai drafts text <name> --about \"<what it is for, with the client's facts>\"";
|
|
338
509
|
};
|
|
339
510
|
};
|
|
511
|
+
readonly styles: {
|
|
512
|
+
readonly prefix: "wb-reply-";
|
|
513
|
+
};
|
|
340
514
|
readonly delivery: {
|
|
341
515
|
readonly choice: "built_in";
|
|
342
516
|
readonly intro: "Writing, summarising and answering questions with Claude, with a monthly spending cap.";
|
|
@@ -454,6 +628,221 @@ export declare const capabilities: {
|
|
|
454
628
|
readonly intro: "Counting page views and the things visitors do on the site.";
|
|
455
629
|
};
|
|
456
630
|
};
|
|
631
|
+
readonly availability: {
|
|
632
|
+
readonly $schema: "./schema.json";
|
|
633
|
+
readonly format: 1;
|
|
634
|
+
readonly component: "availability";
|
|
635
|
+
readonly title: "Availability and pricing";
|
|
636
|
+
readonly 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.";
|
|
637
|
+
readonly status: "built";
|
|
638
|
+
readonly surfaces: {
|
|
639
|
+
readonly package: readonly ["quote", "createAvailability", "availabilityMigration", "calendarAction", "checkSettings", "stayClaims", "staysClash", "todayIn", "AvailabilityError"];
|
|
640
|
+
};
|
|
641
|
+
readonly rules: readonly [{
|
|
642
|
+
readonly id: "availability.quote-is-fact";
|
|
643
|
+
readonly kind: "must";
|
|
644
|
+
readonly 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.";
|
|
645
|
+
readonly enforced_by: "review";
|
|
646
|
+
readonly probe: "availability.quote";
|
|
647
|
+
}, {
|
|
648
|
+
readonly id: "availability.book-through-block";
|
|
649
|
+
readonly kind: "must";
|
|
650
|
+
readonly 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.";
|
|
651
|
+
readonly enforced_by: "database";
|
|
652
|
+
readonly probe: "availability.store";
|
|
653
|
+
}, {
|
|
654
|
+
readonly id: "availability.calendar-days";
|
|
655
|
+
readonly kind: "must";
|
|
656
|
+
readonly 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.";
|
|
657
|
+
readonly enforced_by: "types";
|
|
658
|
+
readonly probe: "availability.quote";
|
|
659
|
+
}, {
|
|
660
|
+
readonly id: "availability.no-names-in-notes";
|
|
661
|
+
readonly kind: "must_not";
|
|
662
|
+
readonly 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.";
|
|
663
|
+
readonly enforced_by: "review";
|
|
664
|
+
readonly probe: "availability.store";
|
|
665
|
+
}, {
|
|
666
|
+
readonly id: "availability.public-gets-the-quote";
|
|
667
|
+
readonly kind: "must_not";
|
|
668
|
+
readonly text: "Let a public page or the anonymous key read availability_blocks: public pages get the finished quote from the server.";
|
|
669
|
+
readonly enforced_by: "database";
|
|
670
|
+
readonly probe: "availability.store";
|
|
671
|
+
}, {
|
|
672
|
+
readonly id: "availability.daily-cleanup";
|
|
673
|
+
readonly kind: "must";
|
|
674
|
+
readonly 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.";
|
|
675
|
+
readonly enforced_by: "review";
|
|
676
|
+
readonly probe: "availability.store";
|
|
677
|
+
}, {
|
|
678
|
+
readonly id: "availability.calendar-as-shipped";
|
|
679
|
+
readonly kind: "must";
|
|
680
|
+
readonly 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.";
|
|
681
|
+
readonly enforced_by: "review";
|
|
682
|
+
readonly probe: "availability.calendar";
|
|
683
|
+
}];
|
|
684
|
+
readonly examples: readonly [{
|
|
685
|
+
readonly id: "availability.example.quote";
|
|
686
|
+
readonly title: "Answer \"is 12 Dec free, and how much?\"";
|
|
687
|
+
readonly lang: "ts";
|
|
688
|
+
readonly 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)', ...]";
|
|
689
|
+
readonly probe: "availability.quote";
|
|
690
|
+
}, {
|
|
691
|
+
readonly id: "availability.example.calendar";
|
|
692
|
+
readonly title: "The owner's tap-to-block calendar";
|
|
693
|
+
readonly lang: "ts";
|
|
694
|
+
readonly 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 })";
|
|
695
|
+
readonly probe: "availability.calendar";
|
|
696
|
+
}];
|
|
697
|
+
readonly probes: readonly [{
|
|
698
|
+
readonly id: "availability.quote";
|
|
699
|
+
readonly half: "runtime";
|
|
700
|
+
readonly expect: "pass";
|
|
701
|
+
readonly smoke: true;
|
|
702
|
+
readonly cost_hint_usd: 0;
|
|
703
|
+
readonly 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";
|
|
704
|
+
}, {
|
|
705
|
+
readonly id: "availability.store";
|
|
706
|
+
readonly half: "runtime";
|
|
707
|
+
readonly expect: "pass";
|
|
708
|
+
readonly cost_hint_usd: 0;
|
|
709
|
+
readonly 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";
|
|
710
|
+
}, {
|
|
711
|
+
readonly id: "availability.calendar";
|
|
712
|
+
readonly half: "runtime";
|
|
713
|
+
readonly expect: "pass";
|
|
714
|
+
readonly cost_hint_usd: 0;
|
|
715
|
+
readonly 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";
|
|
716
|
+
}];
|
|
717
|
+
readonly offer: {
|
|
718
|
+
readonly pattern: "settings";
|
|
719
|
+
readonly 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.";
|
|
720
|
+
readonly 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.";
|
|
721
|
+
readonly answers_file: "workbench/availability.json";
|
|
722
|
+
readonly 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.";
|
|
723
|
+
readonly questions: readonly [{
|
|
724
|
+
readonly id: "availability.what";
|
|
725
|
+
readonly sets: "availability.what";
|
|
726
|
+
readonly ask: "What do people book?";
|
|
727
|
+
readonly style: "table";
|
|
728
|
+
readonly other: {
|
|
729
|
+
readonly kind: "text";
|
|
730
|
+
readonly min: 3;
|
|
731
|
+
readonly max: 200;
|
|
732
|
+
readonly 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\".";
|
|
733
|
+
};
|
|
734
|
+
readonly must_ask: true;
|
|
735
|
+
readonly note: "Only the client knows. Their answer names the resources (one per thing booked separately) and suggests the stay types to propose next.";
|
|
736
|
+
}, {
|
|
737
|
+
readonly id: "availability.stays";
|
|
738
|
+
readonly sets: "availability.stay_types";
|
|
739
|
+
readonly 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?";
|
|
740
|
+
readonly style: "table";
|
|
741
|
+
readonly other: {
|
|
742
|
+
readonly kind: "list";
|
|
743
|
+
readonly min: 1;
|
|
744
|
+
readonly max: 10;
|
|
745
|
+
readonly description: "Each way it's booked, with its start and end time and the others it can't share a date with.";
|
|
746
|
+
};
|
|
747
|
+
readonly must_ask: true;
|
|
748
|
+
readonly 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.";
|
|
749
|
+
}, {
|
|
750
|
+
readonly id: "availability.events";
|
|
751
|
+
readonly sets: "availability.event_types";
|
|
752
|
+
readonly ask: "Should prices depend on the kind of event? Here is the usual list.";
|
|
753
|
+
readonly style: "cards";
|
|
754
|
+
readonly answers: readonly [{
|
|
755
|
+
readonly label: "Yes, use this list";
|
|
756
|
+
readonly value: "list";
|
|
757
|
+
readonly means: "Birthday, debut, wedding, baptism, reunion, team building, other: a rule can add a fee for some of them.";
|
|
758
|
+
}, {
|
|
759
|
+
readonly label: "No, same price for all";
|
|
760
|
+
readonly value: "none";
|
|
761
|
+
readonly means: "No event types; the quote never asks for one.";
|
|
762
|
+
}, {
|
|
763
|
+
readonly label: "Yes, with my own list";
|
|
764
|
+
readonly value: "own";
|
|
765
|
+
readonly means: "Type the kinds of event the business prices differently.";
|
|
766
|
+
}];
|
|
767
|
+
readonly other: {
|
|
768
|
+
readonly kind: "list";
|
|
769
|
+
readonly min: 1;
|
|
770
|
+
readonly max: 20;
|
|
771
|
+
readonly description: "The kinds of event, each a short name.";
|
|
772
|
+
};
|
|
773
|
+
readonly note: "Recommend the usual list for venues and resorts; \"none\" for most other businesses. Write ids in lower case with underscores (team_building).";
|
|
774
|
+
}, {
|
|
775
|
+
readonly id: "availability.holidays";
|
|
776
|
+
readonly sets: "availability.holidays";
|
|
777
|
+
readonly ask: "Should holidays be priced like weekends?";
|
|
778
|
+
readonly style: "cards";
|
|
779
|
+
readonly answers: readonly [{
|
|
780
|
+
readonly label: "Yes, like a weekend";
|
|
781
|
+
readonly value: "weekend";
|
|
782
|
+
readonly means: "A regular holiday or special non-working day on a weekday costs the weekend price.";
|
|
783
|
+
}, {
|
|
784
|
+
readonly label: "No, by the day of the week";
|
|
785
|
+
readonly value: "weekday";
|
|
786
|
+
readonly means: "Holidays cost what their day of the week costs.";
|
|
787
|
+
}, {
|
|
788
|
+
readonly label: "Set holiday prices separately";
|
|
789
|
+
readonly value: "separate";
|
|
790
|
+
readonly means: "Holidays get their own price rules, entered on the owner's pages.";
|
|
791
|
+
}];
|
|
792
|
+
readonly other: {
|
|
793
|
+
readonly kind: "text";
|
|
794
|
+
readonly min: 3;
|
|
795
|
+
readonly max: 200;
|
|
796
|
+
readonly description: "How holidays should be priced, in the client's words.";
|
|
797
|
+
};
|
|
798
|
+
readonly 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.";
|
|
799
|
+
}, {
|
|
800
|
+
readonly id: "availability.holds";
|
|
801
|
+
readonly sets: "availability.hold_days";
|
|
802
|
+
readonly ask: "How long is a date held while waiting for a deposit?";
|
|
803
|
+
readonly style: "cards";
|
|
804
|
+
readonly answers: readonly [{
|
|
805
|
+
readonly label: "3 days";
|
|
806
|
+
readonly value: 3;
|
|
807
|
+
readonly means: "The date shows as held (H) for 3 days, then is free again unless marked booked.";
|
|
808
|
+
}, {
|
|
809
|
+
readonly label: "1 day";
|
|
810
|
+
readonly value: 1;
|
|
811
|
+
}, {
|
|
812
|
+
readonly label: "7 days";
|
|
813
|
+
readonly value: 7;
|
|
814
|
+
}, {
|
|
815
|
+
readonly label: "No holds";
|
|
816
|
+
readonly value: 0;
|
|
817
|
+
readonly means: "A date is free until it is booked; nothing is held.";
|
|
818
|
+
}];
|
|
819
|
+
readonly other: {
|
|
820
|
+
readonly kind: "whole";
|
|
821
|
+
readonly min: 0;
|
|
822
|
+
readonly max: 60;
|
|
823
|
+
readonly description: "Days, 0 to 60.";
|
|
824
|
+
};
|
|
825
|
+
}];
|
|
826
|
+
readonly not_asked: readonly [{
|
|
827
|
+
readonly sets: "availability.prices";
|
|
828
|
+
readonly 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.";
|
|
829
|
+
}, {
|
|
830
|
+
readonly sets: "availability.alternatives";
|
|
831
|
+
readonly 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.";
|
|
832
|
+
}, {
|
|
833
|
+
readonly sets: "availability.currency";
|
|
834
|
+
readonly why: "It is the country's own, from workbench/country.json.";
|
|
835
|
+
}];
|
|
836
|
+
readonly needs: readonly ["country"];
|
|
837
|
+
};
|
|
838
|
+
readonly styles: {
|
|
839
|
+
readonly prefix: "wb-cal-";
|
|
840
|
+
};
|
|
841
|
+
readonly delivery: {
|
|
842
|
+
readonly choice: "optional";
|
|
843
|
+
readonly 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.";
|
|
844
|
+
};
|
|
845
|
+
};
|
|
457
846
|
readonly config: {
|
|
458
847
|
readonly $schema: "./schema.json";
|
|
459
848
|
readonly format: 1;
|
|
@@ -552,7 +941,7 @@ export declare const capabilities: {
|
|
|
552
941
|
readonly summary: "workbench-country writes dates, times, numbers and money the way readers in the Philippines, Australia, the UK and the US expect, from one pack per country, and gives each pack's spelling for the Gateway. It runs in the app itself, with no Gateway call and no cost.";
|
|
553
942
|
readonly status: "built";
|
|
554
943
|
readonly surfaces: {
|
|
555
|
-
readonly package: readonly ["COUNTRY_PACKS", "countryPackFor", "audiencePack", "checkPack", "CountryPackError", "formatDate", "formatDateTime", "calendarDayIn", "formatNumber", "formatMoney", "formattingLocale", "currencySymbol", "currencySymbolFrom", "WORLDWIDE", "isWorldwide"];
|
|
944
|
+
readonly package: readonly ["COUNTRY_PACKS", "countryPackFor", "audiencePack", "checkPack", "CountryPackError", "formatDate", "formatDateTime", "calendarDayIn", "formatNumber", "formatMoney", "formattingLocale", "currencySymbol", "currencySymbolFrom", "WORLDWIDE", "isWorldwide", "HOLIDAYS", "holidaysFor", "holidayOn", "checkHolidays"];
|
|
556
945
|
};
|
|
557
946
|
readonly rules: readonly [{
|
|
558
947
|
readonly id: "country.one-formatter";
|
|
@@ -572,6 +961,12 @@ export declare const capabilities: {
|
|
|
572
961
|
readonly text: "Write an amount with a bare \"$\": Australian dollars are A$ and US dollars US$.";
|
|
573
962
|
readonly enforced_by: "types";
|
|
574
963
|
readonly probe: "country.packs";
|
|
964
|
+
}, {
|
|
965
|
+
readonly id: "country.holidays-from-proclamation";
|
|
966
|
+
readonly kind: "must";
|
|
967
|
+
readonly 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.";
|
|
968
|
+
readonly enforced_by: "review";
|
|
969
|
+
readonly probe: "country.holidays";
|
|
575
970
|
}, {
|
|
576
971
|
readonly id: "country.spelling-from-pack";
|
|
577
972
|
readonly kind: "must";
|
|
@@ -599,6 +994,13 @@ export declare const capabilities: {
|
|
|
599
994
|
readonly expect: "pass";
|
|
600
995
|
readonly cost_hint_usd: 0;
|
|
601
996
|
readonly 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";
|
|
997
|
+
}, {
|
|
998
|
+
readonly id: "country.holidays";
|
|
999
|
+
readonly half: "runtime";
|
|
1000
|
+
readonly expect: "pass";
|
|
1001
|
+
readonly smoke: true;
|
|
1002
|
+
readonly cost_hint_usd: 0;
|
|
1003
|
+
readonly 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";
|
|
602
1004
|
}];
|
|
603
1005
|
readonly offer: {
|
|
604
1006
|
readonly pattern: "project";
|