jsfe 1.4.1 → 1.5.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.
Files changed (43) hide show
  1. package/README.md +25 -0
  2. package/dist/helpers/amount-to-speech.d.ts +6 -0
  3. package/dist/helpers/amount-to-speech.d.ts.map +1 -0
  4. package/dist/helpers/amount-to-speech.js +195 -0
  5. package/dist/helpers/answer.d.ts +50 -0
  6. package/dist/helpers/answer.d.ts.map +1 -0
  7. package/dist/helpers/answer.js +409 -0
  8. package/dist/helpers/capture.d.ts +5 -0
  9. package/dist/helpers/capture.d.ts.map +1 -0
  10. package/dist/helpers/capture.js +50 -0
  11. package/dist/helpers/card-checks.d.ts +8 -0
  12. package/dist/helpers/card-checks.d.ts.map +1 -0
  13. package/dist/helpers/card-checks.js +46 -0
  14. package/dist/helpers/contact.d.ts +50 -0
  15. package/dist/helpers/contact.d.ts.map +1 -0
  16. package/dist/helpers/contact.js +178 -0
  17. package/dist/helpers/date-words.d.ts +2 -0
  18. package/dist/helpers/date-words.d.ts.map +1 -0
  19. package/dist/helpers/date-words.js +29 -0
  20. package/dist/helpers/index.d.ts +72 -0
  21. package/dist/helpers/index.d.ts.map +1 -0
  22. package/dist/helpers/index.js +34 -0
  23. package/dist/helpers/match-inputs.d.ts +28 -0
  24. package/dist/helpers/match-inputs.d.ts.map +1 -0
  25. package/dist/helpers/match-inputs.js +223 -0
  26. package/dist/helpers/matches-choice.d.ts +26 -0
  27. package/dist/helpers/matches-choice.d.ts.map +1 -0
  28. package/dist/helpers/matches-choice.js +61 -0
  29. package/dist/helpers/otp-check.d.ts +36 -0
  30. package/dist/helpers/otp-check.d.ts.map +1 -0
  31. package/dist/helpers/otp-check.js +75 -0
  32. package/dist/helpers/phrases.d.ts +12 -0
  33. package/dist/helpers/phrases.d.ts.map +1 -0
  34. package/dist/helpers/phrases.js +31 -0
  35. package/dist/helpers/url-speech.d.ts +8 -0
  36. package/dist/helpers/url-speech.d.ts.map +1 -0
  37. package/dist/helpers/url-speech.js +28 -0
  38. package/dist/helpers/validators.d.ts +13 -0
  39. package/dist/helpers/validators.d.ts.map +1 -0
  40. package/dist/helpers/validators.js +32 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +2 -1
  43. package/package.json +5 -1
package/README.md CHANGED
@@ -342,6 +342,31 @@ const body = buildAiRequest(
342
342
  - `mergeTextParameters` adds a text model's free-text parameters only when the text model chose
343
343
  the same flow. The classifier's enum and boolean values always win.
344
344
 
345
+ **Flow helpers — `jsfe/helpers`:**
346
+ The functions the bundled system flows (`ask-choice`, `collect-input`, `authenticate-user`…) call from
347
+ expressions, so every host runs the same ones. Pure: no credentials, no network. The engine does not
348
+ register them by itself; a host lists them among its approved functions:
349
+
350
+ ```javascript
351
+ import { STANDARD_FUNCTIONS } from 'jsfe/helpers';
352
+
353
+ const engine = new WorkflowEngine(logger, aiCallback, flows, tools,
354
+ { ...STANDARD_FUNCTIONS, sendSMSOTP, sendEmail /* the host's own, credentialed functions */ });
355
+ ```
356
+
357
+ - Reading answers: `matchesChoice`, `pickChoice`, `isAnswer`, `isAgentRequest`, `isOtherRequest`,
358
+ `isSpelledFragment`, `isAmountAnswer`, `amountInAnswer`, `readEmail`, `readPhone`,
359
+ `readInternationalPhone`, `readIdentifier`, `normalizeAndFindCapture`, and `matchInputs` (which of a
360
+ form's inputs an answer fills; ties between inputs of the same form are returned for the flow to ask).
361
+ - Saying values: `amountToSpeech`, `dateToWords`, `last4`, `maskEmail`, `urlToSpeech`,
362
+ `textWithUrlToSpeech`, `anythingElse`, `goodbye`.
363
+ - Checks: `validateDigits`, `validatePhone`, `validateEmail`, `validateCardNumber`,
364
+ `validateExpiration`, and `validateOTP(code, container)`, which accepts a code whose SHA-256 matches
365
+ `container.otpHash` within 10 minutes of `container.otpTimestamp` and leaves the reason for a refusal
366
+ in `container.otpFailure` (`missing`, `expired` or `mismatch`). Sending the code is the host's job.
367
+ - Session-aware helpers read only the cargo a caller passes: `callerTextNumber`, `textDestination`,
368
+ `callerIsInternational`.
369
+
345
370
  **Alternative AI Services:**
346
371
  You can integrate any AI service (Claude, Gemini, local LLMs, etc.) by implementing this same interface. The engine only requires a function that takes system instructions and user input, then returns an AI response.
347
372
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * An amount for a reply: spoken in words on voice ("Fifty Dollars and Twenty Five Cents", "Cincuenta dólares…"), written
3
+ * as '$1,500.00' on text. amountToSpeech; pure, part of jsfe/helpers.
4
+ */
5
+ export function amountToSpeech(amount: any, language?: string, voice?: boolean): string;
6
+ //# sourceMappingURL=amount-to-speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amount-to-speech.d.ts","sourceRoot":"","sources":["../../src/helpers/amount-to-speech.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wFA4MC"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * An amount for a reply: spoken in words on voice ("Fifty Dollars and Twenty Five Cents", "Cincuenta dólares…"), written
3
+ * as '$1,500.00' on text. amountToSpeech; pure, part of jsfe/helpers.
4
+ */
5
+ export function amountToSpeech(amount, language = 'en', voice = true) {
6
+ try {
7
+ if (!voice) {
8
+ // Written: '$1,500.00' (it was the bare number, '1500.00', in every text reply).
9
+ const n = typeof amount === 'number' ? amount : parseFloat(String(amount).replace(/[^-0-9.]/g, ''));
10
+ return isNaN(n) ? String(amount) : (n < 0 ? '-' : '') + '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
11
+ }
12
+ if (!language) {
13
+ language = 'en';
14
+ }
15
+ if (language === 'en') {
16
+ const units = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
17
+ const tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
18
+ const scales = ["", "Thousand", "Million", "Billion"];
19
+ function convertSmallNumber(num) {
20
+ let words = "";
21
+ if (num >= 100) {
22
+ words += units[Math.floor(num / 100)] + " Hundred ";
23
+ num %= 100;
24
+ }
25
+ if (num >= 20) {
26
+ words += tens[Math.floor(num / 10)] + (num % 10 !== 0 ? " " + units[num % 10] : "") + " ";
27
+ }
28
+ else if (num > 0) {
29
+ words += units[num] + " ";
30
+ }
31
+ return words;
32
+ }
33
+ // 0. Validate and normalize input
34
+ if (typeof amount === 'string') {
35
+ amount = amount.replace(/[^-0-9.]/g, ''); // Keep the minus sign!
36
+ }
37
+ const numAmount = parseFloat(amount);
38
+ if (isNaN(numAmount)) {
39
+ return "Invalid amount";
40
+ }
41
+ if (Math.abs(numAmount) >= 1000000000000) {
42
+ return "Amount exceeds 999 Billion limit";
43
+ }
44
+ // 1. Handle "Negative" prefix and work with absolute value
45
+ const isNegative = numAmount < 0;
46
+ let absoluteAmount = Math.abs(numAmount);
47
+ // 2. Split Dollars and Cents
48
+ const parts = absoluteAmount.toFixed(2).split(".");
49
+ let dollarVal = parseInt(parts[0]);
50
+ let centVal = parseInt(parts[1]);
51
+ // 3. Process Dollars
52
+ let dollarWords = "";
53
+ if (dollarVal === 0) {
54
+ dollarWords = "Zero";
55
+ }
56
+ else {
57
+ let scaleIdx = 0;
58
+ while (dollarVal > 0) {
59
+ let chunk = dollarVal % 1000;
60
+ if (chunk > 0) {
61
+ dollarWords = convertSmallNumber(chunk) + scales[scaleIdx] + " " + dollarWords;
62
+ }
63
+ dollarVal = Math.floor(dollarVal / 1000);
64
+ scaleIdx++;
65
+ }
66
+ }
67
+ // 4. Formatting for TTS
68
+ const dLabel = (parseInt(parts[0]) === 1) ? "Dollar" : "Dollars";
69
+ const cLabel = (centVal === 1) ? "Cent" : "Cents";
70
+ let finalSpeech = (isNegative ? "Negative " : "") + dollarWords.trim() + " " + dLabel;
71
+ if (centVal > 0) {
72
+ const centWords = convertSmallNumber(centVal);
73
+ finalSpeech += " and " + centWords.trim() + " " + cLabel;
74
+ }
75
+ return finalSpeech.replace(/\s+/g, ' ');
76
+ }
77
+ else if (language === 'es') {
78
+ // Spanish implementation
79
+ const basics = ["", "Un", "Dos", "Tres", "Cuatro", "Cinco", "Seis", "Siete", "Ocho", "Nueve", "Diez", "Once", "Doce", "Trece", "Catorce", "Quince", "Dieciséis", "Diecisiete", "Dieciocho", "Diecinueve", "Veinte", "Veintiún", "Veintidós", "Veintitrés", "Veinticuatro", "Veinticinco", "Veintiséis", "Veintisiete", "Veintiocho", "Veintinueve"];
80
+ const tens = ["", "", "Veinte", "Treinta", "Cuarenta", "Cincuenta", "Sesenta", "Setenta", "Ochenta", "Noventa"];
81
+ const hundreds = ["", "Ciento", "Doscientos", "Trescientos", "Cuatrocientos", "Quinientos", "Seiscientos", "Setecientos", "Ochocientos", "Novecientos"];
82
+ function convertGroup(n) {
83
+ let output = "";
84
+ if (n === 100)
85
+ return "Cien";
86
+ if (n >= 100) {
87
+ output += hundreds[Math.floor(n / 100)] + " ";
88
+ n %= 100;
89
+ }
90
+ if (n === 0 && output.length > 0)
91
+ return output.trim();
92
+ if (n === 0)
93
+ return "";
94
+ if (n < 30) {
95
+ output += basics[n];
96
+ }
97
+ else {
98
+ output += tens[Math.floor(n / 10)];
99
+ if ((n % 10) > 0) {
100
+ output += " y " + basics[n % 10];
101
+ }
102
+ }
103
+ return output.trim();
104
+ }
105
+ if (typeof amount === 'string') {
106
+ amount = amount.replace(/[^-0-9.]/g, '');
107
+ }
108
+ const numAmount = parseFloat(amount);
109
+ if (isNaN(numAmount)) {
110
+ return "Cantidad no válida";
111
+ }
112
+ if (Math.abs(numAmount) >= 1000000000000) {
113
+ return "Cantidad excede el límite de 999 billones";
114
+ }
115
+ const isNegative = numAmount < 0;
116
+ let absoluteAmount = Math.abs(numAmount);
117
+ const parts = absoluteAmount.toFixed(2).split(".");
118
+ let dollarVal = parseInt(parts[0]);
119
+ let centVal = parseInt(parts[1]);
120
+ let dollarWords = "";
121
+ if (dollarVal === 0) {
122
+ dollarWords = "Cero";
123
+ }
124
+ else if (dollarVal === 1) {
125
+ dollarWords = "Un";
126
+ }
127
+ else {
128
+ let scaleIdx = 0;
129
+ let tempDollars = dollarVal;
130
+ let accumulated = "";
131
+ while (tempDollars > 0) {
132
+ let chunk = tempDollars % 1000;
133
+ if (chunk > 0) {
134
+ let chunkText = convertGroup(chunk);
135
+ if (scaleIdx === 0) {
136
+ accumulated = chunkText + " " + accumulated;
137
+ }
138
+ else if (scaleIdx === 1) {
139
+ // Thousands
140
+ if (chunk === 1) {
141
+ accumulated = "Mil " + accumulated;
142
+ }
143
+ else {
144
+ accumulated = chunkText + " Mil " + accumulated;
145
+ }
146
+ }
147
+ else if (scaleIdx === 2) {
148
+ // Millions
149
+ if (chunk === 1) {
150
+ accumulated = "Un Millón " + accumulated;
151
+ }
152
+ else {
153
+ accumulated = chunkText + " Millones " + accumulated;
154
+ }
155
+ }
156
+ else if (scaleIdx === 3) {
157
+ // Billions
158
+ if (chunk === 1) {
159
+ accumulated = "Un Billón " + accumulated;
160
+ }
161
+ else {
162
+ accumulated = chunkText + " Billones " + accumulated;
163
+ }
164
+ }
165
+ }
166
+ tempDollars = Math.floor(tempDollars / 1000);
167
+ scaleIdx++;
168
+ }
169
+ dollarWords = accumulated.trim();
170
+ }
171
+ const dLabel = (parseInt(parts[0]) === 1) ? "Dólar" : "Dólares";
172
+ const cLabel = (centVal === 1) ? "Centavo" : "Centavos";
173
+ // Add "de" for exact millions/billions
174
+ if (dollarWords.endsWith("Millón") || dollarWords.endsWith("Millones") || dollarWords.endsWith("Billón") || dollarWords.endsWith("Billones")) {
175
+ dollarWords += " de";
176
+ }
177
+ let finalSpeech = (isNegative ? "Negativo " : "") + dollarWords + " " + dLabel;
178
+ if (centVal > 0) {
179
+ let centWords = "";
180
+ if (centVal === 1)
181
+ centWords = "Un";
182
+ else
183
+ centWords = convertGroup(centVal);
184
+ finalSpeech += " con " + centWords + " " + cLabel;
185
+ }
186
+ return finalSpeech.replace(/\s+/g, ' ').trim();
187
+ }
188
+ else {
189
+ return `Language ${language} not supported for amount to speech conversion`;
190
+ }
191
+ }
192
+ catch (error) {
193
+ return `Error converting amount to speech: ${error.message}`;
194
+ }
195
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Whether the answer is a `kind` answer ('yes' | 'no' | 'agent' | 'exit' | 'text'), with the caller's `extra` words.
3
+ * YES, AGENT and TEXT never match a negated answer ("not sure", "I don't want an agent"); 0 is AGENT, * is EXIT.
4
+ */
5
+ export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
6
+ /** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
7
+ export function isAgentRequest(input: any): boolean;
8
+ /** Whether the answer is (part of) a spelled address — letters said one at a time, or a spelled symbol. */
9
+ export function isSpelledFragment(input: any): boolean;
10
+ /**
11
+ * Whether a free-text answer to a question that expects a `kind` ('number' | 'email' | 'contact') is the caller
12
+ * changing direction ("what's my balance?" to "how much would you like to pay?") rather than an attempt at the answer:
13
+ * three or more words, none of them the expected shape. Such an answer is DISPATCHed, never counted as a failed try.
14
+ * A short miss ("what?") is not: the asking flow re-asks it.
15
+ */
16
+ export function isOtherRequest(input: any, kind: any): boolean;
17
+ /**
18
+ * Whether the answer names an amount ("$1", "1 dollar", "un dólar", "50 cents", "12.50") rather than a keypad key.
19
+ * Such an answer never picks a digit choice (pickChoice): "$1" said at "press 1 to pay what is due" chose FULL and
20
+ * the caller was offered $235.90 (2026-10-14).
21
+ */
22
+ export function isAmountAnswer(input: any): boolean;
23
+ /**
24
+ * The amount an answer names, or NaN when it names none or is ambiguous. Prod replay 2026-10-14: the first number in the
25
+ * answer was taken, so "pagar cuenta 50, ciento treinta y cuatro…" paid $50, "$10 con 18 centavos" paid $10.00 and an
26
+ * offer read back with "terminada en 2 4 8 6" paid $2. Numbers that name an account, card, code or ZIP, and digits spoken
27
+ * one by one, are not amounts; two different remaining numbers are ambiguous unless exactly one is marked as money ($,
28
+ * dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
29
+ */
30
+ export function amountInAnswer(input: any): number;
31
+ /**
32
+ * ask-choice's matcher: the key of the choice the answer picks, '__agent', '__exit', or '' (no choice).
33
+ * choices: [{ key, words, negative?, neutral?, exact? }]. A key (digit, * or #) and a YES word pick a choice only as the whole
34
+ * answer, courtesy aside (onlyAffirmative); an `exact` choice (one that charges or cancels) treats ALL its words that way. A choice keyed 'yes' also takes ANSWER_WORDS.yes (CONFIRM_WORDS when `exact`) and, when not `exact`, a bare thanks (THANKS), and the NO-type
35
+ * choice (key no / decline / done / cancel, or negative: true) also takes ANSWER_WORDS.no, so callers list only their
36
+ * extras. A word after a negation in its clause is negated: it picks the NO-type choice; a `neutral` choice still
37
+ * matches unless it follows its negation within two words; any other choice is blocked and NO answers. A deferral
38
+ * (later, mañana) answers NO; a reply led by a bare 'no' turns YES into NO. With acceptOther, AGENT only when the
39
+ * whole answer is an agent request and EXIT only on exit/quit/salir/abort (cancel is an answer there).
40
+ */
41
+ export function pickChoice(answer: any, choices: any, acceptOther: any): any;
42
+ export namespace ANSWER_WORDS {
43
+ let yes: string[];
44
+ let no: string[];
45
+ let agent: string[];
46
+ let exit: string[];
47
+ let text: string[];
48
+ }
49
+ export const CONFIRM_WORDS: string[];
50
+ //# sourceMappingURL=answer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAyIA;;;GAGG;AACH,wEAgBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDA6BC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AAjWD,qCAKiD"}