@mccaskie-labs/workbench-config 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Message intake: `ai.intake()` reads a social media message (a screenshot, pasted text or both) into one of the project's intake types through the Gateway's `POST /v1/intake/read`. It returns the reply drafter's `IntakeResult` (snake_case, as the Gateway sends it) typed by the project's own types: the type, its fields, what is missing or reads two ways, and `needs_human_check`, so `ai.reply` takes it unchanged. `npx workbench-ai intake generate` checks `workbench/intake/*.json` with the Gateway's own rules and writes `src/intake.generated.ts` (`--check` for Checks). With `save_input: project` it keeps the image and text in the project's own `INTAKE_STORE` bucket; Workbench keeps only the call's facts. `createFakeAi` answers from each type's examples. The `intake` manifest carries its client questions.
8
+
3
9
  ## 0.3.2
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,212 @@
1
+ {
2
+ "$schema": "./schema.json",
3
+ "format": 1,
4
+ "component": "intake",
5
+ "title": "Message intake through the Gateway",
6
+ "summary": "Reads one social media message (a Messenger chat, a group post or a comment), as a screenshot, pasted text or both, into one of the project's own intake types: the type, its fields, what is missing or reads two ways, and whether a person should check it. The types are files in the project's repo, sent with every call. Workbench keeps only the call's facts, never the image or the text; the project can keep them in its own bucket.",
7
+ "status": "built",
8
+ "delivery": {
9
+ "choice": "optional",
10
+ "intro": "Turns a pasted message or a screenshot of a chat into a filled-in form, so enquiries don't have to be typed up by hand."
11
+ },
12
+ "surfaces": {
13
+ "gateway": [
14
+ "POST /v1/intake/read"
15
+ ],
16
+ "package": [
17
+ "Ai.intake",
18
+ "defineIntakeTypes",
19
+ "IntakeTypes",
20
+ "IntakeResult"
21
+ ]
22
+ },
23
+ "config": [
24
+ {
25
+ "name": "WORKBENCH_GATEWAY_KEY",
26
+ "required": true,
27
+ "secret": true,
28
+ "environments": [
29
+ "staging",
30
+ "production"
31
+ ],
32
+ "description": "Project key with the ai scope, delivered as a Worker secret by gw_key_issue"
33
+ }
34
+ ],
35
+ "limits": [
36
+ { "id": "intake.types-per-call", "value": 20, "unit": "types", "per": "request", "enforced_by": "gateway", "refusal": "invalid_request" },
37
+ { "id": "intake.properties-per-type", "value": 40, "unit": "properties", "per": "type", "enforced_by": "gateway", "refusal": "invalid_request" },
38
+ { "id": "intake.definition-size", "value": 16384, "unit": "bytes", "per": "type", "enforced_by": "gateway", "refusal": "invalid_request" },
39
+ { "id": "intake.types-size", "value": 65536, "unit": "bytes", "per": "request", "enforced_by": "gateway", "refusal": "invalid_request" },
40
+ { "id": "intake.text-length", "value": 20000, "unit": "characters", "per": "request", "enforced_by": "gateway", "refusal": "invalid_request" },
41
+ { "id": "intake.image-size", "value": 5242880, "unit": "bytes", "per": "request", "enforced_by": "gateway", "refusal": "invalid_request" },
42
+ { "id": "intake.images-per-call", "value": 1, "unit": "images", "per": "request", "enforced_by": "gateway", "refusal": "invalid_request" }
43
+ ],
44
+ "errors": [
45
+ { "code": "invalid_request", "http": 400, "retryable": false, "meaning": "No text or image, an image too large or of another kind, or a type definition that breaks the rules (the message names the type and the JSON path)" },
46
+ { "code": "budget_exceeded", "http": 402, "retryable": false, "meaning": "The project's budget, its intake cap or the global ceiling is reached" },
47
+ { "code": "content_refused", "http": 422, "retryable": false, "meaning": "The content rules, or Claude itself, refused the message or the reading" },
48
+ { "code": "content_check_unavailable", "http": 503, "retryable": true, "meaning": "The content checker is down, so nothing was read" },
49
+ { "code": "upstream_error", "http": 502, "retryable": true, "meaning": "Anthropic failed after the Gateway's retries" },
50
+ { "code": "upstream_timeout", "http": 504, "retryable": true, "meaning": "Anthropic didn't answer in time" }
51
+ ],
52
+ "rules": [
53
+ {
54
+ "id": "intake.types-from-files",
55
+ "kind": "must",
56
+ "text": "Keep each intake type as a file, workbench/intake/<name>.json, and pass intakeTypes from src/intake.generated.ts. Run npx workbench-ai intake generate after changing one, and keep npx workbench-ai intake generate --check in the project's Checks. Never write definitions inline.",
57
+ "enforced_by": "types",
58
+ "probe": "intake.read.text"
59
+ },
60
+ {
61
+ "id": "intake.human-check",
62
+ "kind": "must",
63
+ "text": "Act on needsHumanCheck: show the reading to a person (or keep it marked to check) before it becomes a booking, a quote or a reply. It is the Gateway's rule, not the model's score.",
64
+ "enforced_by": "review",
65
+ "probe": "intake.read.unknown"
66
+ },
67
+ {
68
+ "id": "intake.personal-marked",
69
+ "kind": "must",
70
+ "text": "Mark every field that holds a personal detail with x-personal (name, handle, phone, email, address, photo). An unmarked field whose name or format looks personal is refused, and personal details the type doesn't ask for are blanked in rawText.",
71
+ "enforced_by": "gateway",
72
+ "probe": "intake.read.personal-blanked"
73
+ },
74
+ {
75
+ "id": "intake.no-content-kept",
76
+ "kind": "must_not",
77
+ "text": "Expect Workbench to keep the image or the text: it keeps only the call's facts. To keep them, set save_input to project in workbench/intake.json and pass store: env.INTAKE_STORE.",
78
+ "enforced_by": "gateway",
79
+ "probe": "intake.read.text"
80
+ },
81
+ {
82
+ "id": "intake.subject-is-app-user",
83
+ "kind": "must_not",
84
+ "text": "Send the poster's name or number as subject or ref. subject is user:<id> of the app's own user; ref is the app's own record id, such as enquiry:4812.",
85
+ "enforced_by": "review",
86
+ "probe": "intake.read.text"
87
+ },
88
+ {
89
+ "id": "intake.bad-type-refused",
90
+ "kind": "must_not",
91
+ "text": "Send a type that breaks the format: the Gateway refuses it with invalid_request before any spend.",
92
+ "enforced_by": "gateway",
93
+ "probe": "intake.read.bad-type-refused"
94
+ }
95
+ ],
96
+ "examples": [
97
+ {
98
+ "id": "intake.example.read",
99
+ "title": "Read a pasted message or a screenshot",
100
+ "lang": "ts",
101
+ "probe": "intake.read.text",
102
+ "code": "import { intakeTypes } from './intake.generated'\n\nconst r = await ai.intake({ types: intakeTypes, text, image, timeZone: country.timeZone.id, job: 'intake.quote-request', subject: `user:${user.id}`, ref: `enquiry:${enquiry.id}`, store: env.INTAKE_STORE })\nif (r.type === 'venue-quote-request') {\n await saveEnquiry({ ...r.data, toCheck: r.needsHumanCheck, missing: r.missing })\n}"
103
+ }
104
+ ],
105
+ "probes": [
106
+ { "id": "intake.read.text", "half": "runtime", "expect": "pass", "smoke": true, "cost_hint_usd": 0.01, "note": "One of HmPo's sample messages as text: the type, the data and no human check" },
107
+ { "id": "intake.read.unknown", "half": "runtime", "expect": "pass", "cost_hint_usd": 0.01, "note": "A thank-you review comes back unknown, with needs_human_check and unknown_type" },
108
+ { "id": "intake.read.personal-blanked", "half": "runtime", "expect": "pass", "cost_hint_usd": 0.01, "note": "A message with a phone number the type doesn't ask for: rawText shows [phone]" },
109
+ { "id": "intake.read.bad-type-refused", "half": "runtime", "expect": "refused_as_expected", "refusal": "invalid_request", "cost_hint_usd": 0, "note": "A type with an unmarked contact_phone field is refused before any spend" }
110
+ ],
111
+ "offer": {
112
+ "pattern": "settings",
113
+ "when": "The client gets enquiries, orders or posts as messages (Messenger, a Facebook group, comments) and wants them read into a form or a list instead of typed up by hand.",
114
+ "say": "I'll set this up with Workbench message intake: you paste a message or a screenshot and it fills in the details, and flags the ones a person should check. Workbench never keeps the message. Here are the details I'd pull out; tell me what to change.",
115
+ "answers_file": "workbench/intake/<name>.json",
116
+ "per": "kind of message",
117
+ "generator": "npx workbench-ai intake generate",
118
+ "answers_format": "One type per file, docs/message-intake.md §3: `{ \"format\": 1, \"name\", \"version\": 1, \"description\" (the client's words, made plain), \"not_this\", \"examples\" (1 to 5 real-looking messages with the data each should give), \"glossary\" (the trade's own shorthand), \"schema\" (the fields from intake.fields; required lists what the app can't do without), \"confidence_floor\", \"model\", \"save_input\" }`. The project's own setting is workbench/intake.json, `{ \"format\": 1, \"save_input\": \"none\" | \"project\" }`. Then run the generator and commit src/intake.generated.ts.",
119
+ "questions": [
120
+ {
121
+ "id": "intake.kind",
122
+ "sets": ["name", "description"],
123
+ "ask": "What kind of message do you want read?",
124
+ "style": "table",
125
+ "other": {
126
+ "kind": "text",
127
+ "min": 3,
128
+ "max": 200,
129
+ "description": "In the client's words, such as \"quote requests on Messenger\" or \"orders in comments under our posts\"."
130
+ },
131
+ "must_ask": true,
132
+ "note": "Only the client knows. Name the type after it in lower case with hyphens (venue-quote-request). Ask for two or three real messages (names blanked) to use as examples."
133
+ },
134
+ {
135
+ "id": "intake.fields",
136
+ "sets": ["schema", "examples"],
137
+ "ask": "Here are the details I'd pull out of each message. Keep them, change some, or add or remove?",
138
+ "style": "table",
139
+ "other": {
140
+ "kind": "list",
141
+ "min": 1,
142
+ "max": 40,
143
+ "description": "Each detail: its name, what it is, whether the app can't do without it, and the choices if it is one of a list."
144
+ },
145
+ "must_ask": true,
146
+ "note": "Propose from intake.kind, as a table of field, meaning, kind (text, number, date, one of a list), and needed. For an enquiry: the date, how many people, what for, budget, extras. Mark as needed only what the app can't act without; the rest are optional."
147
+ },
148
+ {
149
+ "id": "intake.personal",
150
+ "sets": "schema.x-personal",
151
+ "ask": "Should it keep the person's name or contact details?",
152
+ "style": "cards",
153
+ "answers": [
154
+ { "label": "No, leave them out", "value": "none", "means": "Names, numbers and handles are blanked in what it reads, and never kept." },
155
+ { "label": "Their display name only", "value": "name", "means": "The name shown on the message, so the owner knows who asked." },
156
+ { "label": "Name and how to reach them", "value": "contact", "means": "The name and a phone number or email when the message gives one." }
157
+ ],
158
+ "must_ask": true,
159
+ "note": "Each kept detail becomes a field marked x-personal with its kind (name, phone, email). Say that what the app keeps is under its own privacy notice."
160
+ },
161
+ {
162
+ "id": "intake.save",
163
+ "sets": "save_input",
164
+ "ask": "Should your app keep a copy of each message (the screenshot and its text)?",
165
+ "style": "cards",
166
+ "answers": [
167
+ { "label": "No, read it and let it go", "value": "none", "means": "Only the filled-in details are kept, by your app." },
168
+ { "label": "Yes, in your own storage", "value": "project", "means": "Your app keeps the screenshot and text in its own private bucket. Workbench never keeps it." }
169
+ ],
170
+ "must_ask": true,
171
+ "note": "project sets save_input in workbench/intake.json and needs the INTAKE_STORE bucket bound to the app (a private R2 bucket <id>-intake per environment, in the projects account). Pass store: env.INTAKE_STORE on every call. Say who will be able to see the copies and for how long."
172
+ },
173
+ {
174
+ "id": "intake.check",
175
+ "sets": "app.when_unsure",
176
+ "ask": "When it isn't sure, what should the app do?",
177
+ "style": "cards",
178
+ "answers": [
179
+ { "label": "Ask you to check first", "value": "ask", "means": "The details wait for you to confirm before they are saved." },
180
+ { "label": "Save it, marked to check", "value": "mark", "means": "It is saved straight away with a \"to check\" mark." },
181
+ { "label": "Skip it", "value": "skip", "means": "Unsure readings aren't saved; you read the message yourself." }
182
+ ],
183
+ "note": "This is the app's own behaviour when needsHumanCheck is true; nothing in the type file changes."
184
+ },
185
+ {
186
+ "id": "intake.model",
187
+ "sets": "model",
188
+ "ask": "How careful should the reading be?",
189
+ "style": "cards",
190
+ "answers": [
191
+ { "label": "Careful", "value": "claude-default", "means": "About US$0.01 a message." },
192
+ { "label": "Quick and cheap", "value": "claude-fast", "means": "Cheaper, and misses more in long or messy chats." }
193
+ ],
194
+ "note": "Recommend careful. Leave model out of the file for claude-default."
195
+ }
196
+ ],
197
+ "not_asked": [
198
+ {
199
+ "sets": "confidence_floor",
200
+ "why": "0.75 suits a start; it is tuned from real results, not asked."
201
+ },
202
+ {
203
+ "sets": "glossary",
204
+ "why": "Claude adds the trade's own shorthand from the client's example messages (DU for day use); the Gateway's built-in Filipino and Taglish glossary covers everyday words."
205
+ },
206
+ {
207
+ "sets": "budget",
208
+ "why": "Intake has its own monthly cap in the Gateway, set with Control's gw_budget_set (tool intake); offer it only when the client asks about cost."
209
+ }
210
+ ]
211
+ }
212
+ }
@@ -36,7 +36,8 @@
36
36
  "ui",
37
37
  "forms",
38
38
  "sign-in",
39
- "availability"
39
+ "availability",
40
+ "intake"
40
41
  ]
41
42
  },
42
43
  "title": {
@@ -432,7 +433,8 @@
432
433
  "ui",
433
434
  "forms",
434
435
  "sign-in",
435
- "availability"
436
+ "availability",
437
+ "intake"
436
438
  ]
437
439
  }
438
440
  },
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "4a52a6ba9520";
1
+ export declare const MANIFEST_VERSION: "7ad8cef4de58";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -2895,6 +2895,297 @@ export declare const capabilities: {
2895
2895
  readonly intro: "Pictures made by AI from a description.";
2896
2896
  };
2897
2897
  };
2898
+ readonly intake: {
2899
+ readonly $schema: "./schema.json";
2900
+ readonly format: 1;
2901
+ readonly component: "intake";
2902
+ readonly title: "Message intake through the Gateway";
2903
+ readonly summary: "Reads one social media message (a Messenger chat, a group post or a comment), as a screenshot, pasted text or both, into one of the project's own intake types: the type, its fields, what is missing or reads two ways, and whether a person should check it. The types are files in the project's repo, sent with every call. Workbench keeps only the call's facts, never the image or the text; the project can keep them in its own bucket.";
2904
+ readonly status: "built";
2905
+ readonly surfaces: {
2906
+ readonly gateway: readonly ["POST /v1/intake/read"];
2907
+ readonly package: readonly ["Ai.intake", "defineIntakeTypes", "IntakeTypes", "IntakeResult"];
2908
+ };
2909
+ readonly config: readonly [{
2910
+ readonly name: "WORKBENCH_GATEWAY_KEY";
2911
+ readonly required: true;
2912
+ readonly secret: true;
2913
+ readonly environments: readonly ["staging", "production"];
2914
+ readonly description: "Project key with the ai scope, delivered as a Worker secret by gw_key_issue";
2915
+ }];
2916
+ readonly limits: readonly [{
2917
+ readonly id: "intake.types-per-call";
2918
+ readonly value: 20;
2919
+ readonly unit: "types";
2920
+ readonly per: "request";
2921
+ readonly enforced_by: "gateway";
2922
+ readonly refusal: "invalid_request";
2923
+ }, {
2924
+ readonly id: "intake.properties-per-type";
2925
+ readonly value: 40;
2926
+ readonly unit: "properties";
2927
+ readonly per: "type";
2928
+ readonly enforced_by: "gateway";
2929
+ readonly refusal: "invalid_request";
2930
+ }, {
2931
+ readonly id: "intake.definition-size";
2932
+ readonly value: 16384;
2933
+ readonly unit: "bytes";
2934
+ readonly per: "type";
2935
+ readonly enforced_by: "gateway";
2936
+ readonly refusal: "invalid_request";
2937
+ }, {
2938
+ readonly id: "intake.types-size";
2939
+ readonly value: 65536;
2940
+ readonly unit: "bytes";
2941
+ readonly per: "request";
2942
+ readonly enforced_by: "gateway";
2943
+ readonly refusal: "invalid_request";
2944
+ }, {
2945
+ readonly id: "intake.text-length";
2946
+ readonly value: 20000;
2947
+ readonly unit: "characters";
2948
+ readonly per: "request";
2949
+ readonly enforced_by: "gateway";
2950
+ readonly refusal: "invalid_request";
2951
+ }, {
2952
+ readonly id: "intake.image-size";
2953
+ readonly value: 5242880;
2954
+ readonly unit: "bytes";
2955
+ readonly per: "request";
2956
+ readonly enforced_by: "gateway";
2957
+ readonly refusal: "invalid_request";
2958
+ }, {
2959
+ readonly id: "intake.images-per-call";
2960
+ readonly value: 1;
2961
+ readonly unit: "images";
2962
+ readonly per: "request";
2963
+ readonly enforced_by: "gateway";
2964
+ readonly refusal: "invalid_request";
2965
+ }];
2966
+ readonly errors: readonly [{
2967
+ readonly code: "invalid_request";
2968
+ readonly http: 400;
2969
+ readonly retryable: false;
2970
+ readonly meaning: "No text or image, an image too large or of another kind, or a type definition that breaks the rules (the message names the type and the JSON path)";
2971
+ }, {
2972
+ readonly code: "budget_exceeded";
2973
+ readonly http: 402;
2974
+ readonly retryable: false;
2975
+ readonly meaning: "The project's budget, its intake cap or the global ceiling is reached";
2976
+ }, {
2977
+ readonly code: "content_refused";
2978
+ readonly http: 422;
2979
+ readonly retryable: false;
2980
+ readonly meaning: "The content rules, or Claude itself, refused the message or the reading";
2981
+ }, {
2982
+ readonly code: "content_check_unavailable";
2983
+ readonly http: 503;
2984
+ readonly retryable: true;
2985
+ readonly meaning: "The content checker is down, so nothing was read";
2986
+ }, {
2987
+ readonly code: "upstream_error";
2988
+ readonly http: 502;
2989
+ readonly retryable: true;
2990
+ readonly meaning: "Anthropic failed after the Gateway's retries";
2991
+ }, {
2992
+ readonly code: "upstream_timeout";
2993
+ readonly http: 504;
2994
+ readonly retryable: true;
2995
+ readonly meaning: "Anthropic didn't answer in time";
2996
+ }];
2997
+ readonly rules: readonly [{
2998
+ readonly id: "intake.types-from-files";
2999
+ readonly kind: "must";
3000
+ readonly text: "Keep each intake type as a file, workbench/intake/<name>.json, and pass intakeTypes from src/intake.generated.ts. Run npx workbench-ai intake generate after changing one, and keep npx workbench-ai intake generate --check in the project's Checks. Never write definitions inline.";
3001
+ readonly enforced_by: "types";
3002
+ readonly probe: "intake.read.text";
3003
+ }, {
3004
+ readonly id: "intake.human-check";
3005
+ readonly kind: "must";
3006
+ readonly text: "Act on needsHumanCheck: show the reading to a person (or keep it marked to check) before it becomes a booking, a quote or a reply. It is the Gateway's rule, not the model's score.";
3007
+ readonly enforced_by: "review";
3008
+ readonly probe: "intake.read.unknown";
3009
+ }, {
3010
+ readonly id: "intake.personal-marked";
3011
+ readonly kind: "must";
3012
+ readonly text: "Mark every field that holds a personal detail with x-personal (name, handle, phone, email, address, photo). An unmarked field whose name or format looks personal is refused, and personal details the type doesn't ask for are blanked in rawText.";
3013
+ readonly enforced_by: "gateway";
3014
+ readonly probe: "intake.read.personal-blanked";
3015
+ }, {
3016
+ readonly id: "intake.no-content-kept";
3017
+ readonly kind: "must_not";
3018
+ readonly text: "Expect Workbench to keep the image or the text: it keeps only the call's facts. To keep them, set save_input to project in workbench/intake.json and pass store: env.INTAKE_STORE.";
3019
+ readonly enforced_by: "gateway";
3020
+ readonly probe: "intake.read.text";
3021
+ }, {
3022
+ readonly id: "intake.subject-is-app-user";
3023
+ readonly kind: "must_not";
3024
+ readonly text: "Send the poster's name or number as subject or ref. subject is user:<id> of the app's own user; ref is the app's own record id, such as enquiry:4812.";
3025
+ readonly enforced_by: "review";
3026
+ readonly probe: "intake.read.text";
3027
+ }, {
3028
+ readonly id: "intake.bad-type-refused";
3029
+ readonly kind: "must_not";
3030
+ readonly text: "Send a type that breaks the format: the Gateway refuses it with invalid_request before any spend.";
3031
+ readonly enforced_by: "gateway";
3032
+ readonly probe: "intake.read.bad-type-refused";
3033
+ }];
3034
+ readonly examples: readonly [{
3035
+ readonly id: "intake.example.read";
3036
+ readonly title: "Read a pasted message or a screenshot";
3037
+ readonly lang: "ts";
3038
+ readonly code: "import { intakeTypes } from './intake.generated'\n\nconst r = await ai.intake({ types: intakeTypes, text, image, timeZone: country.timeZone.id, job: 'intake.quote-request', subject: `user:${user.id}`, ref: `enquiry:${enquiry.id}`, store: env.INTAKE_STORE })\nif (r.type === 'venue-quote-request') {\n await saveEnquiry({ ...r.data, toCheck: r.needsHumanCheck, missing: r.missing })\n}";
3039
+ readonly probe: "intake.read.text";
3040
+ }];
3041
+ readonly probes: readonly [{
3042
+ readonly id: "intake.read.text";
3043
+ readonly half: "runtime";
3044
+ readonly expect: "pass";
3045
+ readonly smoke: true;
3046
+ readonly cost_hint_usd: 0.01;
3047
+ readonly note: "One of HmPo's sample messages as text: the type, the data and no human check";
3048
+ }, {
3049
+ readonly id: "intake.read.unknown";
3050
+ readonly half: "runtime";
3051
+ readonly expect: "pass";
3052
+ readonly cost_hint_usd: 0.01;
3053
+ readonly note: "A thank-you review comes back unknown, with needs_human_check and unknown_type";
3054
+ }, {
3055
+ readonly id: "intake.read.personal-blanked";
3056
+ readonly half: "runtime";
3057
+ readonly expect: "pass";
3058
+ readonly cost_hint_usd: 0.01;
3059
+ readonly note: "A message with a phone number the type doesn't ask for: rawText shows [phone]";
3060
+ }, {
3061
+ readonly id: "intake.read.bad-type-refused";
3062
+ readonly half: "runtime";
3063
+ readonly expect: "refused_as_expected";
3064
+ readonly refusal: "invalid_request";
3065
+ readonly cost_hint_usd: 0;
3066
+ readonly note: "A type with an unmarked contact_phone field is refused before any spend";
3067
+ }];
3068
+ readonly offer: {
3069
+ readonly pattern: "settings";
3070
+ readonly when: "The client gets enquiries, orders or posts as messages (Messenger, a Facebook group, comments) and wants them read into a form or a list instead of typed up by hand.";
3071
+ readonly say: "I'll set this up with Workbench message intake: you paste a message or a screenshot and it fills in the details, and flags the ones a person should check. Workbench never keeps the message. Here are the details I'd pull out; tell me what to change.";
3072
+ readonly answers_file: "workbench/intake/<name>.json";
3073
+ readonly per: "kind of message";
3074
+ readonly generator: "npx workbench-ai intake generate";
3075
+ readonly answers_format: "One type per file, docs/message-intake.md §3: `{ \"format\": 1, \"name\", \"version\": 1, \"description\" (the client's words, made plain), \"not_this\", \"examples\" (1 to 5 real-looking messages with the data each should give), \"glossary\" (the trade's own shorthand), \"schema\" (the fields from intake.fields; required lists what the app can't do without), \"confidence_floor\", \"model\", \"save_input\" }`. The project's own setting is workbench/intake.json, `{ \"format\": 1, \"save_input\": \"none\" | \"project\" }`. Then run the generator and commit src/intake.generated.ts.";
3076
+ readonly questions: readonly [{
3077
+ readonly id: "intake.kind";
3078
+ readonly sets: readonly ["name", "description"];
3079
+ readonly ask: "What kind of message do you want read?";
3080
+ readonly style: "table";
3081
+ readonly other: {
3082
+ readonly kind: "text";
3083
+ readonly min: 3;
3084
+ readonly max: 200;
3085
+ readonly description: "In the client's words, such as \"quote requests on Messenger\" or \"orders in comments under our posts\".";
3086
+ };
3087
+ readonly must_ask: true;
3088
+ readonly note: "Only the client knows. Name the type after it in lower case with hyphens (venue-quote-request). Ask for two or three real messages (names blanked) to use as examples.";
3089
+ }, {
3090
+ readonly id: "intake.fields";
3091
+ readonly sets: readonly ["schema", "examples"];
3092
+ readonly ask: "Here are the details I'd pull out of each message. Keep them, change some, or add or remove?";
3093
+ readonly style: "table";
3094
+ readonly other: {
3095
+ readonly kind: "list";
3096
+ readonly min: 1;
3097
+ readonly max: 40;
3098
+ readonly description: "Each detail: its name, what it is, whether the app can't do without it, and the choices if it is one of a list.";
3099
+ };
3100
+ readonly must_ask: true;
3101
+ readonly note: "Propose from intake.kind, as a table of field, meaning, kind (text, number, date, one of a list), and needed. For an enquiry: the date, how many people, what for, budget, extras. Mark as needed only what the app can't act without; the rest are optional.";
3102
+ }, {
3103
+ readonly id: "intake.personal";
3104
+ readonly sets: "schema.x-personal";
3105
+ readonly ask: "Should it keep the person's name or contact details?";
3106
+ readonly style: "cards";
3107
+ readonly answers: readonly [{
3108
+ readonly label: "No, leave them out";
3109
+ readonly value: "none";
3110
+ readonly means: "Names, numbers and handles are blanked in what it reads, and never kept.";
3111
+ }, {
3112
+ readonly label: "Their display name only";
3113
+ readonly value: "name";
3114
+ readonly means: "The name shown on the message, so the owner knows who asked.";
3115
+ }, {
3116
+ readonly label: "Name and how to reach them";
3117
+ readonly value: "contact";
3118
+ readonly means: "The name and a phone number or email when the message gives one.";
3119
+ }];
3120
+ readonly must_ask: true;
3121
+ readonly note: "Each kept detail becomes a field marked x-personal with its kind (name, phone, email). Say that what the app keeps is under its own privacy notice.";
3122
+ }, {
3123
+ readonly id: "intake.save";
3124
+ readonly sets: "save_input";
3125
+ readonly ask: "Should your app keep a copy of each message (the screenshot and its text)?";
3126
+ readonly style: "cards";
3127
+ readonly answers: readonly [{
3128
+ readonly label: "No, read it and let it go";
3129
+ readonly value: "none";
3130
+ readonly means: "Only the filled-in details are kept, by your app.";
3131
+ }, {
3132
+ readonly label: "Yes, in your own storage";
3133
+ readonly value: "project";
3134
+ readonly means: "Your app keeps the screenshot and text in its own private bucket. Workbench never keeps it.";
3135
+ }];
3136
+ readonly must_ask: true;
3137
+ readonly note: "project sets save_input in workbench/intake.json and needs the INTAKE_STORE bucket bound to the app (a private R2 bucket <id>-intake per environment, in the projects account). Pass store: env.INTAKE_STORE on every call. Say who will be able to see the copies and for how long.";
3138
+ }, {
3139
+ readonly id: "intake.check";
3140
+ readonly sets: "app.when_unsure";
3141
+ readonly ask: "When it isn't sure, what should the app do?";
3142
+ readonly style: "cards";
3143
+ readonly answers: readonly [{
3144
+ readonly label: "Ask you to check first";
3145
+ readonly value: "ask";
3146
+ readonly means: "The details wait for you to confirm before they are saved.";
3147
+ }, {
3148
+ readonly label: "Save it, marked to check";
3149
+ readonly value: "mark";
3150
+ readonly means: "It is saved straight away with a \"to check\" mark.";
3151
+ }, {
3152
+ readonly label: "Skip it";
3153
+ readonly value: "skip";
3154
+ readonly means: "Unsure readings aren't saved; you read the message yourself.";
3155
+ }];
3156
+ readonly note: "This is the app's own behaviour when needsHumanCheck is true; nothing in the type file changes.";
3157
+ }, {
3158
+ readonly id: "intake.model";
3159
+ readonly sets: "model";
3160
+ readonly ask: "How careful should the reading be?";
3161
+ readonly style: "cards";
3162
+ readonly answers: readonly [{
3163
+ readonly label: "Careful";
3164
+ readonly value: "claude-default";
3165
+ readonly means: "About US$0.01 a message.";
3166
+ }, {
3167
+ readonly label: "Quick and cheap";
3168
+ readonly value: "claude-fast";
3169
+ readonly means: "Cheaper, and misses more in long or messy chats.";
3170
+ }];
3171
+ readonly note: "Recommend careful. Leave model out of the file for claude-default.";
3172
+ }];
3173
+ readonly not_asked: readonly [{
3174
+ readonly sets: "confidence_floor";
3175
+ readonly why: "0.75 suits a start; it is tuned from real results, not asked.";
3176
+ }, {
3177
+ readonly sets: "glossary";
3178
+ readonly why: "Claude adds the trade's own shorthand from the client's example messages (DU for day use); the Gateway's built-in Filipino and Taglish glossary covers everyday words.";
3179
+ }, {
3180
+ readonly sets: "budget";
3181
+ readonly why: "Intake has its own monthly cap in the Gateway, set with Control's gw_budget_set (tool intake); offer it only when the client asks about cost.";
3182
+ }];
3183
+ };
3184
+ readonly delivery: {
3185
+ readonly choice: "optional";
3186
+ readonly intro: "Turns a pasted message or a screenshot of a chat into a filled-in form, so enquiries don't have to be typed up by hand.";
3187
+ };
3188
+ };
2898
3189
  readonly pdf: {
2899
3190
  readonly $schema: "./schema.json";
2900
3191
  readonly format: 1;
@@ -1,6 +1,6 @@
1
1
  // Generated by `npm run capabilities:build` from capabilities/*.json. Do not edit.
2
2
  // check:capabilities fails in Checks when this file is stale.
3
- export const MANIFEST_VERSION = "4a52a6ba9520";
3
+ export const MANIFEST_VERSION = "7ad8cef4de58";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -3799,6 +3799,369 @@ export const capabilities = {
3799
3799
  "intro": "Pictures made by AI from a description."
3800
3800
  }
3801
3801
  },
3802
+ "intake": {
3803
+ "$schema": "./schema.json",
3804
+ "format": 1,
3805
+ "component": "intake",
3806
+ "title": "Message intake through the Gateway",
3807
+ "summary": "Reads one social media message (a Messenger chat, a group post or a comment), as a screenshot, pasted text or both, into one of the project's own intake types: the type, its fields, what is missing or reads two ways, and whether a person should check it. The types are files in the project's repo, sent with every call. Workbench keeps only the call's facts, never the image or the text; the project can keep them in its own bucket.",
3808
+ "status": "built",
3809
+ "surfaces": {
3810
+ "gateway": [
3811
+ "POST /v1/intake/read"
3812
+ ],
3813
+ "package": [
3814
+ "Ai.intake",
3815
+ "defineIntakeTypes",
3816
+ "IntakeTypes",
3817
+ "IntakeResult"
3818
+ ]
3819
+ },
3820
+ "config": [
3821
+ {
3822
+ "name": "WORKBENCH_GATEWAY_KEY",
3823
+ "required": true,
3824
+ "secret": true,
3825
+ "environments": [
3826
+ "staging",
3827
+ "production"
3828
+ ],
3829
+ "description": "Project key with the ai scope, delivered as a Worker secret by gw_key_issue"
3830
+ }
3831
+ ],
3832
+ "limits": [
3833
+ {
3834
+ "id": "intake.types-per-call",
3835
+ "value": 20,
3836
+ "unit": "types",
3837
+ "per": "request",
3838
+ "enforced_by": "gateway",
3839
+ "refusal": "invalid_request"
3840
+ },
3841
+ {
3842
+ "id": "intake.properties-per-type",
3843
+ "value": 40,
3844
+ "unit": "properties",
3845
+ "per": "type",
3846
+ "enforced_by": "gateway",
3847
+ "refusal": "invalid_request"
3848
+ },
3849
+ {
3850
+ "id": "intake.definition-size",
3851
+ "value": 16384,
3852
+ "unit": "bytes",
3853
+ "per": "type",
3854
+ "enforced_by": "gateway",
3855
+ "refusal": "invalid_request"
3856
+ },
3857
+ {
3858
+ "id": "intake.types-size",
3859
+ "value": 65536,
3860
+ "unit": "bytes",
3861
+ "per": "request",
3862
+ "enforced_by": "gateway",
3863
+ "refusal": "invalid_request"
3864
+ },
3865
+ {
3866
+ "id": "intake.text-length",
3867
+ "value": 20000,
3868
+ "unit": "characters",
3869
+ "per": "request",
3870
+ "enforced_by": "gateway",
3871
+ "refusal": "invalid_request"
3872
+ },
3873
+ {
3874
+ "id": "intake.image-size",
3875
+ "value": 5242880,
3876
+ "unit": "bytes",
3877
+ "per": "request",
3878
+ "enforced_by": "gateway",
3879
+ "refusal": "invalid_request"
3880
+ },
3881
+ {
3882
+ "id": "intake.images-per-call",
3883
+ "value": 1,
3884
+ "unit": "images",
3885
+ "per": "request",
3886
+ "enforced_by": "gateway",
3887
+ "refusal": "invalid_request"
3888
+ }
3889
+ ],
3890
+ "errors": [
3891
+ {
3892
+ "code": "invalid_request",
3893
+ "http": 400,
3894
+ "retryable": false,
3895
+ "meaning": "No text or image, an image too large or of another kind, or a type definition that breaks the rules (the message names the type and the JSON path)"
3896
+ },
3897
+ {
3898
+ "code": "budget_exceeded",
3899
+ "http": 402,
3900
+ "retryable": false,
3901
+ "meaning": "The project's budget, its intake cap or the global ceiling is reached"
3902
+ },
3903
+ {
3904
+ "code": "content_refused",
3905
+ "http": 422,
3906
+ "retryable": false,
3907
+ "meaning": "The content rules, or Claude itself, refused the message or the reading"
3908
+ },
3909
+ {
3910
+ "code": "content_check_unavailable",
3911
+ "http": 503,
3912
+ "retryable": true,
3913
+ "meaning": "The content checker is down, so nothing was read"
3914
+ },
3915
+ {
3916
+ "code": "upstream_error",
3917
+ "http": 502,
3918
+ "retryable": true,
3919
+ "meaning": "Anthropic failed after the Gateway's retries"
3920
+ },
3921
+ {
3922
+ "code": "upstream_timeout",
3923
+ "http": 504,
3924
+ "retryable": true,
3925
+ "meaning": "Anthropic didn't answer in time"
3926
+ }
3927
+ ],
3928
+ "rules": [
3929
+ {
3930
+ "id": "intake.types-from-files",
3931
+ "kind": "must",
3932
+ "text": "Keep each intake type as a file, workbench/intake/<name>.json, and pass intakeTypes from src/intake.generated.ts. Run npx workbench-ai intake generate after changing one, and keep npx workbench-ai intake generate --check in the project's Checks. Never write definitions inline.",
3933
+ "enforced_by": "types",
3934
+ "probe": "intake.read.text"
3935
+ },
3936
+ {
3937
+ "id": "intake.human-check",
3938
+ "kind": "must",
3939
+ "text": "Act on needsHumanCheck: show the reading to a person (or keep it marked to check) before it becomes a booking, a quote or a reply. It is the Gateway's rule, not the model's score.",
3940
+ "enforced_by": "review",
3941
+ "probe": "intake.read.unknown"
3942
+ },
3943
+ {
3944
+ "id": "intake.personal-marked",
3945
+ "kind": "must",
3946
+ "text": "Mark every field that holds a personal detail with x-personal (name, handle, phone, email, address, photo). An unmarked field whose name or format looks personal is refused, and personal details the type doesn't ask for are blanked in rawText.",
3947
+ "enforced_by": "gateway",
3948
+ "probe": "intake.read.personal-blanked"
3949
+ },
3950
+ {
3951
+ "id": "intake.no-content-kept",
3952
+ "kind": "must_not",
3953
+ "text": "Expect Workbench to keep the image or the text: it keeps only the call's facts. To keep them, set save_input to project in workbench/intake.json and pass store: env.INTAKE_STORE.",
3954
+ "enforced_by": "gateway",
3955
+ "probe": "intake.read.text"
3956
+ },
3957
+ {
3958
+ "id": "intake.subject-is-app-user",
3959
+ "kind": "must_not",
3960
+ "text": "Send the poster's name or number as subject or ref. subject is user:<id> of the app's own user; ref is the app's own record id, such as enquiry:4812.",
3961
+ "enforced_by": "review",
3962
+ "probe": "intake.read.text"
3963
+ },
3964
+ {
3965
+ "id": "intake.bad-type-refused",
3966
+ "kind": "must_not",
3967
+ "text": "Send a type that breaks the format: the Gateway refuses it with invalid_request before any spend.",
3968
+ "enforced_by": "gateway",
3969
+ "probe": "intake.read.bad-type-refused"
3970
+ }
3971
+ ],
3972
+ "examples": [
3973
+ {
3974
+ "id": "intake.example.read",
3975
+ "title": "Read a pasted message or a screenshot",
3976
+ "lang": "ts",
3977
+ "code": "import { intakeTypes } from './intake.generated'\n\nconst r = await ai.intake({ types: intakeTypes, text, image, timeZone: country.timeZone.id, job: 'intake.quote-request', subject: `user:${user.id}`, ref: `enquiry:${enquiry.id}`, store: env.INTAKE_STORE })\nif (r.type === 'venue-quote-request') {\n await saveEnquiry({ ...r.data, toCheck: r.needsHumanCheck, missing: r.missing })\n}",
3978
+ "probe": "intake.read.text"
3979
+ }
3980
+ ],
3981
+ "probes": [
3982
+ {
3983
+ "id": "intake.read.text",
3984
+ "half": "runtime",
3985
+ "expect": "pass",
3986
+ "smoke": true,
3987
+ "cost_hint_usd": 0.01,
3988
+ "note": "One of HmPo's sample messages as text: the type, the data and no human check"
3989
+ },
3990
+ {
3991
+ "id": "intake.read.unknown",
3992
+ "half": "runtime",
3993
+ "expect": "pass",
3994
+ "cost_hint_usd": 0.01,
3995
+ "note": "A thank-you review comes back unknown, with needs_human_check and unknown_type"
3996
+ },
3997
+ {
3998
+ "id": "intake.read.personal-blanked",
3999
+ "half": "runtime",
4000
+ "expect": "pass",
4001
+ "cost_hint_usd": 0.01,
4002
+ "note": "A message with a phone number the type doesn't ask for: rawText shows [phone]"
4003
+ },
4004
+ {
4005
+ "id": "intake.read.bad-type-refused",
4006
+ "half": "runtime",
4007
+ "expect": "refused_as_expected",
4008
+ "refusal": "invalid_request",
4009
+ "cost_hint_usd": 0,
4010
+ "note": "A type with an unmarked contact_phone field is refused before any spend"
4011
+ }
4012
+ ],
4013
+ "offer": {
4014
+ "pattern": "settings",
4015
+ "when": "The client gets enquiries, orders or posts as messages (Messenger, a Facebook group, comments) and wants them read into a form or a list instead of typed up by hand.",
4016
+ "say": "I'll set this up with Workbench message intake: you paste a message or a screenshot and it fills in the details, and flags the ones a person should check. Workbench never keeps the message. Here are the details I'd pull out; tell me what to change.",
4017
+ "answers_file": "workbench/intake/<name>.json",
4018
+ "per": "kind of message",
4019
+ "generator": "npx workbench-ai intake generate",
4020
+ "answers_format": "One type per file, docs/message-intake.md §3: `{ \"format\": 1, \"name\", \"version\": 1, \"description\" (the client's words, made plain), \"not_this\", \"examples\" (1 to 5 real-looking messages with the data each should give), \"glossary\" (the trade's own shorthand), \"schema\" (the fields from intake.fields; required lists what the app can't do without), \"confidence_floor\", \"model\", \"save_input\" }`. The project's own setting is workbench/intake.json, `{ \"format\": 1, \"save_input\": \"none\" | \"project\" }`. Then run the generator and commit src/intake.generated.ts.",
4021
+ "questions": [
4022
+ {
4023
+ "id": "intake.kind",
4024
+ "sets": [
4025
+ "name",
4026
+ "description"
4027
+ ],
4028
+ "ask": "What kind of message do you want read?",
4029
+ "style": "table",
4030
+ "other": {
4031
+ "kind": "text",
4032
+ "min": 3,
4033
+ "max": 200,
4034
+ "description": "In the client's words, such as \"quote requests on Messenger\" or \"orders in comments under our posts\"."
4035
+ },
4036
+ "must_ask": true,
4037
+ "note": "Only the client knows. Name the type after it in lower case with hyphens (venue-quote-request). Ask for two or three real messages (names blanked) to use as examples."
4038
+ },
4039
+ {
4040
+ "id": "intake.fields",
4041
+ "sets": [
4042
+ "schema",
4043
+ "examples"
4044
+ ],
4045
+ "ask": "Here are the details I'd pull out of each message. Keep them, change some, or add or remove?",
4046
+ "style": "table",
4047
+ "other": {
4048
+ "kind": "list",
4049
+ "min": 1,
4050
+ "max": 40,
4051
+ "description": "Each detail: its name, what it is, whether the app can't do without it, and the choices if it is one of a list."
4052
+ },
4053
+ "must_ask": true,
4054
+ "note": "Propose from intake.kind, as a table of field, meaning, kind (text, number, date, one of a list), and needed. For an enquiry: the date, how many people, what for, budget, extras. Mark as needed only what the app can't act without; the rest are optional."
4055
+ },
4056
+ {
4057
+ "id": "intake.personal",
4058
+ "sets": "schema.x-personal",
4059
+ "ask": "Should it keep the person's name or contact details?",
4060
+ "style": "cards",
4061
+ "answers": [
4062
+ {
4063
+ "label": "No, leave them out",
4064
+ "value": "none",
4065
+ "means": "Names, numbers and handles are blanked in what it reads, and never kept."
4066
+ },
4067
+ {
4068
+ "label": "Their display name only",
4069
+ "value": "name",
4070
+ "means": "The name shown on the message, so the owner knows who asked."
4071
+ },
4072
+ {
4073
+ "label": "Name and how to reach them",
4074
+ "value": "contact",
4075
+ "means": "The name and a phone number or email when the message gives one."
4076
+ }
4077
+ ],
4078
+ "must_ask": true,
4079
+ "note": "Each kept detail becomes a field marked x-personal with its kind (name, phone, email). Say that what the app keeps is under its own privacy notice."
4080
+ },
4081
+ {
4082
+ "id": "intake.save",
4083
+ "sets": "save_input",
4084
+ "ask": "Should your app keep a copy of each message (the screenshot and its text)?",
4085
+ "style": "cards",
4086
+ "answers": [
4087
+ {
4088
+ "label": "No, read it and let it go",
4089
+ "value": "none",
4090
+ "means": "Only the filled-in details are kept, by your app."
4091
+ },
4092
+ {
4093
+ "label": "Yes, in your own storage",
4094
+ "value": "project",
4095
+ "means": "Your app keeps the screenshot and text in its own private bucket. Workbench never keeps it."
4096
+ }
4097
+ ],
4098
+ "must_ask": true,
4099
+ "note": "project sets save_input in workbench/intake.json and needs the INTAKE_STORE bucket bound to the app (a private R2 bucket <id>-intake per environment, in the projects account). Pass store: env.INTAKE_STORE on every call. Say who will be able to see the copies and for how long."
4100
+ },
4101
+ {
4102
+ "id": "intake.check",
4103
+ "sets": "app.when_unsure",
4104
+ "ask": "When it isn't sure, what should the app do?",
4105
+ "style": "cards",
4106
+ "answers": [
4107
+ {
4108
+ "label": "Ask you to check first",
4109
+ "value": "ask",
4110
+ "means": "The details wait for you to confirm before they are saved."
4111
+ },
4112
+ {
4113
+ "label": "Save it, marked to check",
4114
+ "value": "mark",
4115
+ "means": "It is saved straight away with a \"to check\" mark."
4116
+ },
4117
+ {
4118
+ "label": "Skip it",
4119
+ "value": "skip",
4120
+ "means": "Unsure readings aren't saved; you read the message yourself."
4121
+ }
4122
+ ],
4123
+ "note": "This is the app's own behaviour when needsHumanCheck is true; nothing in the type file changes."
4124
+ },
4125
+ {
4126
+ "id": "intake.model",
4127
+ "sets": "model",
4128
+ "ask": "How careful should the reading be?",
4129
+ "style": "cards",
4130
+ "answers": [
4131
+ {
4132
+ "label": "Careful",
4133
+ "value": "claude-default",
4134
+ "means": "About US$0.01 a message."
4135
+ },
4136
+ {
4137
+ "label": "Quick and cheap",
4138
+ "value": "claude-fast",
4139
+ "means": "Cheaper, and misses more in long or messy chats."
4140
+ }
4141
+ ],
4142
+ "note": "Recommend careful. Leave model out of the file for claude-default."
4143
+ }
4144
+ ],
4145
+ "not_asked": [
4146
+ {
4147
+ "sets": "confidence_floor",
4148
+ "why": "0.75 suits a start; it is tuned from real results, not asked."
4149
+ },
4150
+ {
4151
+ "sets": "glossary",
4152
+ "why": "Claude adds the trade's own shorthand from the client's example messages (DU for day use); the Gateway's built-in Filipino and Taglish glossary covers everyday words."
4153
+ },
4154
+ {
4155
+ "sets": "budget",
4156
+ "why": "Intake has its own monthly cap in the Gateway, set with Control's gw_budget_set (tool intake); offer it only when the client asks about cost."
4157
+ }
4158
+ ]
4159
+ },
4160
+ "delivery": {
4161
+ "choice": "optional",
4162
+ "intro": "Turns a pasted message or a screenshot of a chat into a filled-in form, so enquiries don't have to be typed up by hand."
4163
+ }
4164
+ },
3802
4165
  "pdf": {
3803
4166
  "$schema": "./schema.json",
3804
4167
  "format": 1,
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms", "sign-in", "availability"];
2
+ export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings", "ui", "forms", "sign-in", "availability", "intake"];
3
3
  export type Component = (typeof COMPONENTS)[number];
4
4
  export declare const allowedSchemas: {
5
5
  gateway: z.ZodObject<{
@@ -177,6 +177,7 @@ export declare const capabilitySchema: z.ZodObject<{
177
177
  github: "github";
178
178
  "image-search": "image-search";
179
179
  images: "images";
180
+ intake: "intake";
180
181
  kv: "kv";
181
182
  pdf: "pdf";
182
183
  r2: "r2";
@@ -309,6 +310,7 @@ export declare const capabilitySchema: z.ZodObject<{
309
310
  github: "github";
310
311
  "image-search": "image-search";
311
312
  images: "images";
313
+ intake: "intake";
312
314
  kv: "kv";
313
315
  pdf: "pdf";
314
316
  r2: "r2";
@@ -27,6 +27,7 @@ export const COMPONENTS = [
27
27
  "forms",
28
28
  "sign-in",
29
29
  "availability",
30
+ "intake",
30
31
  ];
31
32
  const id = z.string().regex(/^[a-z0-9][a-z0-9.-]*$/, "lower case, digits, dots and hyphens");
32
33
  const code = z.string().regex(/^[a-z][a-z0-9_]*$/, "a snake_case error code");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",