shopstack 0.3.3 → 0.3.5
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/README.md +27 -9
- package/SKILL.md +16 -7
- package/package.json +1 -1
- package/specs/openapi.json +176 -8
- package/src/cli.js +330 -17
- package/src/client.d.ts +65 -2
- package/src/client.js +56 -3
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ guest restaurant reservations.
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npm install shopstack@0.3.
|
|
9
|
+
npm install shopstack@0.3.5
|
|
10
10
|
# or
|
|
11
|
-
npm install -g shopstack@0.3.
|
|
11
|
+
npm install -g shopstack@0.3.5
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Node.js 18 or newer is required.
|
|
@@ -18,7 +18,7 @@ WebSocket (Node.js 22 or newer). It reads the authenticated checkout resource
|
|
|
18
18
|
after each notification. Older runtimes, or a failed socket connection, use the
|
|
19
19
|
bounded HTTP update wait. The one-time socket token is never placed in a URL.
|
|
20
20
|
|
|
21
|
-
This preview release pairs `shopstack@0.3.
|
|
21
|
+
This preview release pairs `shopstack@0.3.5` with `shopstack-mcp@0.2.5`.
|
|
22
22
|
Both packages use the `preview` distribution tag. The `latest` versions are
|
|
23
23
|
unchanged. Use the exact versions above to install this release.
|
|
24
24
|
|
|
@@ -79,7 +79,7 @@ Configure any stdio MCP client to run:
|
|
|
79
79
|
"mcpServers": {
|
|
80
80
|
"shopstack": {
|
|
81
81
|
"command": "npx",
|
|
82
|
-
"args": ["-y", "shopstack-mcp@0.2.
|
|
82
|
+
"args": ["-y", "shopstack-mcp@0.2.5"],
|
|
83
83
|
"env": {
|
|
84
84
|
"SHOPSTACK_API_URL": "https://shopstack-release-preview.shopstack.workers.dev/v1",
|
|
85
85
|
"SHOPSTACK_CONFIG_FILE": "/absolute/private/path/shopstack-config.json"
|
|
@@ -110,7 +110,7 @@ one MCP server and three focused skills:
|
|
|
110
110
|
- `shopstack-checkout` for checkout creation and monitoring;
|
|
111
111
|
- `shopstack-onboarding` for verified signup and private local profiles.
|
|
112
112
|
|
|
113
|
-
The plugin pins `shopstack-mcp@0.2.
|
|
113
|
+
The plugin pins `shopstack-mcp@0.2.5`, which installs the matching
|
|
114
114
|
reservation-capable `shopstack` client. Configure the API address
|
|
115
115
|
before starting the plugin or use a profile already bound to that API. The existing top-level
|
|
116
116
|
`SKILL.md` remains in the npm client package for compatibility.
|
|
@@ -129,6 +129,11 @@ responds, checkout resumes automatically. No “done” message is needed. The C
|
|
|
129
129
|
keeps watching; MCP agents use `wait_checkout_update` with the latest
|
|
130
130
|
`presentation_revision`. Final order approval is still a separate step.
|
|
131
131
|
|
|
132
|
+
For ordinary missing information, MCP returns `question.content` with the
|
|
133
|
+
current unanswered question. Show it and reply with `send_message`. Interactive
|
|
134
|
+
CLI checkout prompts use the same message. This reads the existing message API;
|
|
135
|
+
it does not add another model call or include the full conversation history.
|
|
136
|
+
|
|
132
137
|
## Run a guest reservation
|
|
133
138
|
|
|
134
139
|
Create `reservation.json` with the diner details, location name, and a detailed
|
|
@@ -156,14 +161,27 @@ shopstack reservation run --file reservation.json
|
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
The CLI asks natural follow-up questions and displays current location,
|
|
159
|
-
restaurant, and time IDs.
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
restaurant, and time IDs. The two-letter diner country is for the E.164 phone
|
|
165
|
+
number, not for the restaurant location. Select a restaurant by number, name,
|
|
166
|
+
or a natural description. The CLI then shows its full description, rating and
|
|
167
|
+
review count, cuisine, price, address, photos, fit notes, caveats, and current
|
|
168
|
+
times. Use `back`, `more`, `search TEXT`, or `refine TEXT` before booking. It
|
|
169
|
+
shows the exact locked booking summary before it asks for confirmation. Any
|
|
170
|
+
clear affirmative answer can confirm it; the exact word `book` is not required.
|
|
171
|
+
After success, the CLI prints a short confirmation and offers immediate
|
|
172
|
+
cancellation. Add `--json` to retain the full machine-readable response.
|
|
173
|
+
|
|
174
|
+
If the input email belongs to a registered OpenTable account, Shopstack uses
|
|
175
|
+
one temporary guest address and forwards provider email to the supplied address.
|
|
176
|
+
Every booking requests reservation SMS, but this does not prove SMS delivery.
|
|
177
|
+
A clear confirmation is sent through the same reservation message route; there
|
|
178
|
+
is no separate reservation approval command.
|
|
162
179
|
Use `shopstack reservation get`, `message`, or `cancel` for individual API
|
|
163
180
|
operations. Use `shopstack reservation options RESERVATION_ID` for another
|
|
164
181
|
stored page and `shopstack reservation option RESERVATION_ID OPTION_ID` for
|
|
165
182
|
full provider details. These reads do not repeat model inference or restaurant
|
|
166
|
-
search.
|
|
183
|
+
search. Use `shopstack reservation email RESERVATION_ID` to inspect owned test
|
|
184
|
+
email evidence without exposing private inbox or forwarding identifiers.
|
|
167
185
|
|
|
168
186
|
## Run a checkout
|
|
169
187
|
|
package/SKILL.md
CHANGED
|
@@ -19,8 +19,8 @@ chat to fix an environment mismatch.
|
|
|
19
19
|
For the JavaScript client and CLI:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm install shopstack@0.3.
|
|
23
|
-
npm install -g shopstack@0.3.
|
|
22
|
+
npm install shopstack@0.3.5
|
|
23
|
+
npm install -g shopstack@0.3.5
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
For MCP clients, configure the local stdio server:
|
|
@@ -30,7 +30,7 @@ For MCP clients, configure the local stdio server:
|
|
|
30
30
|
"mcpServers": {
|
|
31
31
|
"shopstack": {
|
|
32
32
|
"command": "npx",
|
|
33
|
-
"args": ["-y", "shopstack-mcp@0.2.
|
|
33
|
+
"args": ["-y", "shopstack-mcp@0.2.5"]
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -43,7 +43,7 @@ not equivalent to the current source.
|
|
|
43
43
|
|
|
44
44
|
## Run a guest restaurant reservation
|
|
45
45
|
|
|
46
|
-
Give Shopstack the diner name, phone number, two-letter country, a location
|
|
46
|
+
Give Shopstack the diner name, E.164 phone number, its two-letter country, a location
|
|
47
47
|
name, and one detailed natural-language request. Do not supply coordinates or a
|
|
48
48
|
time zone. Shopstack resolves the valid OpenTable location and its time zone.
|
|
49
49
|
Include as much useful detail as possible: date, local time or range, party
|
|
@@ -62,15 +62,23 @@ stored provider description, photos, facts, and time IDs. These reads do not
|
|
|
62
62
|
run another model inference or OpenTable search. Preserve every current
|
|
63
63
|
`option_id` and `time_id`. A new search makes older IDs stale.
|
|
64
64
|
|
|
65
|
+
Evaluate restaurants against the complete request. Compare the rating with its
|
|
66
|
+
review count, cuisine, full description, price, current times, photos, fit
|
|
67
|
+
facts, and caveats. Call `get_reservation_option` for every restaurant used in
|
|
68
|
+
the final comparison. State the observed facts that support the recommendation.
|
|
69
|
+
|
|
65
70
|
Selection rechecks availability, locks the slot, and returns one exact booking
|
|
66
71
|
summary. Send a clear confirmation only when the user confirms that exact
|
|
67
72
|
summary. This confirmation is an ordinary reservation message and can create
|
|
68
73
|
the booking. There is no separate reservation approval endpoint or MCP tool.
|
|
69
74
|
An ambiguous reply must not be treated as confirmation. The anonymous guest
|
|
70
75
|
profile is created only when booking. Pass optional `diner.email` when the user
|
|
71
|
-
supplies it
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
supplies it. If OpenTable reports that this address belongs to a registered
|
|
77
|
+
account, Shopstack retries once with a reservation inbox and forwards provider
|
|
78
|
+
email to the supplied address. Otherwise omit it and Shopstack creates a
|
|
79
|
+
managed inbox when booking. Every booking asks OpenTable for reservation SMS.
|
|
80
|
+
`sms_requested` confirms the request, not SMS delivery. Never require the user
|
|
81
|
+
to give a restaurant email. Cancel a confirmed
|
|
74
82
|
reservation with the typed reservation cancellation operation.
|
|
75
83
|
|
|
76
84
|
## Choose the account shape
|
|
@@ -169,6 +177,7 @@ authoritative fields to the user when they change:
|
|
|
169
177
|
- `activity`: bounded present-tense display text;
|
|
170
178
|
- `intent.name`, `intent.phase`, and `intent.updated_at`: bounded mechanical action progress with no arguments or reasoning;
|
|
171
179
|
- `required_input`: a typed handoff for Link or protected payment-card input;
|
|
180
|
+
- `question.content`: the current ordinary question, when MCP returns it; show it and send the user's answer with `send_message`;
|
|
172
181
|
- `approval`: the exact amount-bound approval request when present.
|
|
173
182
|
|
|
174
183
|
Keep calling `wait_checkout_update` while Link sign-in or approval is pending;
|
package/package.json
CHANGED
package/specs/openapi.json
CHANGED
|
@@ -548,6 +548,39 @@
|
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
},
|
|
551
|
+
"/reservations/{reservation_id}/email": {
|
|
552
|
+
"get": {
|
|
553
|
+
"operationId": "getReservationEmailEvidence",
|
|
554
|
+
"summary": "Read the owned reservation inbox for testing",
|
|
555
|
+
"description": "Returns messages from the reservation-scoped OpenInbox inbox and their forwarding state. It never returns the temporary address, inbox ID, destination address, guest profile ID, or provider message ID.",
|
|
556
|
+
"parameters": [
|
|
557
|
+
{
|
|
558
|
+
"$ref": "#/components/parameters/ReservationId"
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
"responses": {
|
|
562
|
+
"200": {
|
|
563
|
+
"description": "Current reservation email evidence",
|
|
564
|
+
"content": {
|
|
565
|
+
"application/json": {
|
|
566
|
+
"schema": {
|
|
567
|
+
"$ref": "#/components/schemas/ReservationEmailEvidence"
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
"403": {
|
|
573
|
+
"$ref": "#/components/responses/Forbidden"
|
|
574
|
+
},
|
|
575
|
+
"404": {
|
|
576
|
+
"$ref": "#/components/responses/NotFound"
|
|
577
|
+
},
|
|
578
|
+
"503": {
|
|
579
|
+
"$ref": "#/components/responses/Unavailable"
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
551
584
|
"/reservations/{reservation_id}/options": {
|
|
552
585
|
"get": {
|
|
553
586
|
"operationId": "listReservationOptions",
|
|
@@ -1868,11 +1901,12 @@
|
|
|
1868
1901
|
"type": "string",
|
|
1869
1902
|
"format": "email",
|
|
1870
1903
|
"maxLength": 254,
|
|
1871
|
-
"description": "Optional booking contact email.
|
|
1904
|
+
"description": "Optional booking contact email. Shopstack first uses this trimmed address for the guest profile. If OpenTable reports that it belongs to a registered account, Shopstack creates one reservation inbox, retries guest creation once, and forwards provider messages to this address. If omitted, Shopstack uses a managed reservation inbox. Guest creation starts only after exact booking confirmation."
|
|
1872
1905
|
},
|
|
1873
1906
|
"country": {
|
|
1874
1907
|
"type": "string",
|
|
1875
|
-
"pattern": "^[A-Z]{2}$"
|
|
1908
|
+
"pattern": "^[A-Z]{2}$",
|
|
1909
|
+
"description": "ISO 3166-1 alpha-2 country for phone_number, not the restaurant location."
|
|
1876
1910
|
},
|
|
1877
1911
|
"first_name": {
|
|
1878
1912
|
"type": "string",
|
|
@@ -1886,8 +1920,8 @@
|
|
|
1886
1920
|
},
|
|
1887
1921
|
"phone_number": {
|
|
1888
1922
|
"type": "string",
|
|
1889
|
-
"
|
|
1890
|
-
"
|
|
1923
|
+
"pattern": "^\\+[1-9][0-9]{6,14}$",
|
|
1924
|
+
"description": "E.164 phone number compatible with diner.country."
|
|
1891
1925
|
}
|
|
1892
1926
|
}
|
|
1893
1927
|
},
|
|
@@ -2013,7 +2047,6 @@
|
|
|
2013
2047
|
},
|
|
2014
2048
|
"times": {
|
|
2015
2049
|
"type": "array",
|
|
2016
|
-
"minItems": 1,
|
|
2017
2050
|
"maxItems": 30,
|
|
2018
2051
|
"items": {
|
|
2019
2052
|
"$ref": "#/components/schemas/ReservationTime"
|
|
@@ -2094,7 +2127,6 @@
|
|
|
2094
2127
|
},
|
|
2095
2128
|
"available_times": {
|
|
2096
2129
|
"type": "array",
|
|
2097
|
-
"minItems": 1,
|
|
2098
2130
|
"maxItems": 30,
|
|
2099
2131
|
"items": {
|
|
2100
2132
|
"$ref": "#/components/schemas/ReservationTime"
|
|
@@ -2187,6 +2219,17 @@
|
|
|
2187
2219
|
},
|
|
2188
2220
|
"message": {
|
|
2189
2221
|
"type": "string"
|
|
2222
|
+
},
|
|
2223
|
+
"error_code": {
|
|
2224
|
+
"type": "string",
|
|
2225
|
+
"enum": [
|
|
2226
|
+
"BOOKING_REJECTED",
|
|
2227
|
+
"BOOKING_UNKNOWN",
|
|
2228
|
+
"DINER_ALREADY_REGISTERED",
|
|
2229
|
+
"DINER_PHONE_INVALID",
|
|
2230
|
+
"DINER_PROFILE_INVALID",
|
|
2231
|
+
"UPSTREAM_UNAVAILABLE"
|
|
2232
|
+
]
|
|
2190
2233
|
}
|
|
2191
2234
|
}
|
|
2192
2235
|
},
|
|
@@ -2367,7 +2410,9 @@
|
|
|
2367
2410
|
"restaurant",
|
|
2368
2411
|
"date_time",
|
|
2369
2412
|
"party_size",
|
|
2370
|
-
"status"
|
|
2413
|
+
"status",
|
|
2414
|
+
"sms_requested",
|
|
2415
|
+
"email_delivery_mode"
|
|
2371
2416
|
],
|
|
2372
2417
|
"properties": {
|
|
2373
2418
|
"confirmation_number": {
|
|
@@ -2387,6 +2432,19 @@
|
|
|
2387
2432
|
"status": {
|
|
2388
2433
|
"type": "string",
|
|
2389
2434
|
"description": "Shopstack booking outcome, confirmed. Provider lifecycle labels are not returned as this status."
|
|
2435
|
+
},
|
|
2436
|
+
"sms_requested": {
|
|
2437
|
+
"type": "boolean",
|
|
2438
|
+
"const": true,
|
|
2439
|
+
"description": "OpenTable received the reservation SMS request. This does not prove delivery."
|
|
2440
|
+
},
|
|
2441
|
+
"email_delivery_mode": {
|
|
2442
|
+
"type": "string",
|
|
2443
|
+
"enum": [
|
|
2444
|
+
"direct",
|
|
2445
|
+
"shopstack_forwarded",
|
|
2446
|
+
"shopstack_managed"
|
|
2447
|
+
]
|
|
2390
2448
|
}
|
|
2391
2449
|
}
|
|
2392
2450
|
}
|
|
@@ -2476,6 +2534,112 @@
|
|
|
2476
2534
|
"type": "string",
|
|
2477
2535
|
"format": "date-time",
|
|
2478
2536
|
"description": "Search conversation deadline; not the confirmed booking lifetime."
|
|
2537
|
+
},
|
|
2538
|
+
"email_delivery": {
|
|
2539
|
+
"type": "object",
|
|
2540
|
+
"additionalProperties": false,
|
|
2541
|
+
"required": [
|
|
2542
|
+
"mode",
|
|
2543
|
+
"status"
|
|
2544
|
+
],
|
|
2545
|
+
"properties": {
|
|
2546
|
+
"mode": {
|
|
2547
|
+
"type": "string",
|
|
2548
|
+
"const": "shopstack_forwarded"
|
|
2549
|
+
},
|
|
2550
|
+
"status": {
|
|
2551
|
+
"type": "string",
|
|
2552
|
+
"enum": [
|
|
2553
|
+
"pending",
|
|
2554
|
+
"sent",
|
|
2555
|
+
"failed"
|
|
2556
|
+
]
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
},
|
|
2562
|
+
"ReservationEmailEvidence": {
|
|
2563
|
+
"type": "object",
|
|
2564
|
+
"additionalProperties": false,
|
|
2565
|
+
"required": [
|
|
2566
|
+
"reservation_id",
|
|
2567
|
+
"email_delivery_mode",
|
|
2568
|
+
"messages"
|
|
2569
|
+
],
|
|
2570
|
+
"properties": {
|
|
2571
|
+
"reservation_id": {
|
|
2572
|
+
"$ref": "#/components/schemas/ReservationId"
|
|
2573
|
+
},
|
|
2574
|
+
"email_delivery_mode": {
|
|
2575
|
+
"type": "string",
|
|
2576
|
+
"enum": [
|
|
2577
|
+
"direct",
|
|
2578
|
+
"shopstack_forwarded",
|
|
2579
|
+
"shopstack_managed"
|
|
2580
|
+
]
|
|
2581
|
+
},
|
|
2582
|
+
"messages": {
|
|
2583
|
+
"type": "array",
|
|
2584
|
+
"maxItems": 100,
|
|
2585
|
+
"items": {
|
|
2586
|
+
"type": "object",
|
|
2587
|
+
"additionalProperties": false,
|
|
2588
|
+
"required": [
|
|
2589
|
+
"from",
|
|
2590
|
+
"subject",
|
|
2591
|
+
"received_at",
|
|
2592
|
+
"forwarding"
|
|
2593
|
+
],
|
|
2594
|
+
"properties": {
|
|
2595
|
+
"from": {
|
|
2596
|
+
"type": "string"
|
|
2597
|
+
},
|
|
2598
|
+
"subject": {
|
|
2599
|
+
"type": "string"
|
|
2600
|
+
},
|
|
2601
|
+
"received_at": {
|
|
2602
|
+
"type": "string",
|
|
2603
|
+
"format": "date-time"
|
|
2604
|
+
},
|
|
2605
|
+
"text_body": {
|
|
2606
|
+
"type": "string"
|
|
2607
|
+
},
|
|
2608
|
+
"html_body": {
|
|
2609
|
+
"type": "string"
|
|
2610
|
+
},
|
|
2611
|
+
"forwarding": {
|
|
2612
|
+
"type": "object",
|
|
2613
|
+
"additionalProperties": false,
|
|
2614
|
+
"required": [
|
|
2615
|
+
"status",
|
|
2616
|
+
"attempt_count"
|
|
2617
|
+
],
|
|
2618
|
+
"properties": {
|
|
2619
|
+
"status": {
|
|
2620
|
+
"type": "string",
|
|
2621
|
+
"enum": [
|
|
2622
|
+
"pending",
|
|
2623
|
+
"sent",
|
|
2624
|
+
"failed",
|
|
2625
|
+
"not_applicable"
|
|
2626
|
+
]
|
|
2627
|
+
},
|
|
2628
|
+
"attempt_count": {
|
|
2629
|
+
"type": "integer",
|
|
2630
|
+
"minimum": 0
|
|
2631
|
+
},
|
|
2632
|
+
"last_error_category": {
|
|
2633
|
+
"type": "string"
|
|
2634
|
+
},
|
|
2635
|
+
"sent_at": {
|
|
2636
|
+
"type": "string",
|
|
2637
|
+
"format": "date-time"
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2479
2643
|
}
|
|
2480
2644
|
}
|
|
2481
2645
|
},
|
|
@@ -2681,7 +2845,7 @@
|
|
|
2681
2845
|
"enum": [
|
|
2682
2846
|
"qwen"
|
|
2683
2847
|
],
|
|
2684
|
-
"description": "Optional
|
|
2848
|
+
"description": "Optional experimental selector for direct Cerebras qwen-3.8-27b using the managed runner credential. Omit it to use the trusted deployment default. No arbitrary model, endpoint, or credential is accepted."
|
|
2685
2849
|
},
|
|
2686
2850
|
"proxy": {
|
|
2687
2851
|
"$ref": "#/components/schemas/ProxyRequest"
|
|
@@ -2999,6 +3163,10 @@
|
|
|
2999
3163
|
"stage": {
|
|
3000
3164
|
"type": "string",
|
|
3001
3165
|
"enum": [
|
|
3166
|
+
"startup",
|
|
3167
|
+
"register",
|
|
3168
|
+
"finalize",
|
|
3169
|
+
"close",
|
|
3002
3170
|
"read",
|
|
3003
3171
|
"create",
|
|
3004
3172
|
"validate_contract",
|
package/src/cli.js
CHANGED
|
@@ -24,8 +24,9 @@ Payment connection:
|
|
|
24
24
|
|
|
25
25
|
Reservation:
|
|
26
26
|
shopstack reservation create --file reservation.json
|
|
27
|
-
shopstack reservation run --file reservation.json
|
|
27
|
+
shopstack reservation run --file reservation.json [--json]
|
|
28
28
|
shopstack reservation get RESERVATION_ID
|
|
29
|
+
shopstack reservation email RESERVATION_ID [--json]
|
|
29
30
|
shopstack reservation options RESERVATION_ID [--offset N] [--limit N]
|
|
30
31
|
shopstack reservation option RESERVATION_ID OPTION_ID
|
|
31
32
|
shopstack reservation message RESERVATION_ID --revision N --content TEXT
|
|
@@ -40,7 +41,7 @@ Checkout:
|
|
|
40
41
|
shopstack checkout cancel CHECKOUT_ID
|
|
41
42
|
`;
|
|
42
43
|
|
|
43
|
-
function parseOptions(args, allowed) {
|
|
44
|
+
function parseOptions(args, allowed, flags = new Set()) {
|
|
44
45
|
const positional = [];
|
|
45
46
|
const options = {};
|
|
46
47
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -51,6 +52,10 @@ function parseOptions(args, allowed) {
|
|
|
51
52
|
}
|
|
52
53
|
const name = value.slice(2);
|
|
53
54
|
if (!allowed.has(name)) throw new Error(`Unknown option: --${name}`);
|
|
55
|
+
if (flags.has(name)) {
|
|
56
|
+
options[name] = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
54
59
|
const optionValue = args[index + 1];
|
|
55
60
|
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
56
61
|
throw new Error(`Option --${name} requires a value.`);
|
|
@@ -91,7 +96,10 @@ function boundedIntegerOption(options, name, fallback, minimum, maximum) {
|
|
|
91
96
|
|
|
92
97
|
function isAffirmative(value) {
|
|
93
98
|
const answer = String(value).trim().toLowerCase();
|
|
94
|
-
|
|
99
|
+
if (/\b(?:no|not|don't|do not|cancel|stop)\b/u.test(answer)) return false;
|
|
100
|
+
return /^(?:y|yes|yeah|yep|sure|confirm|confirmed|proceed|go ahead|please do|looks good|that works|book it)(?:\b|[.!])/u.test(
|
|
101
|
+
answer,
|
|
102
|
+
);
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
function writeJson(stream, value) {
|
|
@@ -253,18 +261,261 @@ async function reportSignupProgress(progress, stream) {
|
|
|
253
261
|
}
|
|
254
262
|
}
|
|
255
263
|
|
|
264
|
+
function reportReservationOptions(options, stream, offset = 0) {
|
|
265
|
+
options.forEach((option, index) => {
|
|
266
|
+
const rating =
|
|
267
|
+
option.rating === undefined
|
|
268
|
+
? "rating not supplied"
|
|
269
|
+
: `${option.rating}${option.review_count === undefined ? "" : ` (${option.review_count} reviews)`}`;
|
|
270
|
+
stream.write(
|
|
271
|
+
`${offset + index + 1}. ${option.name} — ${rating}${option.price_label === undefined ? "" : ` — ${option.price_label}`}\n`,
|
|
272
|
+
);
|
|
273
|
+
stream.write(` Option ID: ${option.option_id}\n`);
|
|
274
|
+
stream.write(` ${option.address}\n`);
|
|
275
|
+
if (option.cuisines?.length > 0) {
|
|
276
|
+
stream.write(` Cuisine: ${option.cuisines.join(", ")}\n`);
|
|
277
|
+
}
|
|
278
|
+
option.available_times?.forEach((time, timeIndex) => {
|
|
279
|
+
stream.write(
|
|
280
|
+
` Time ${timeIndex + 1}: ${time.date_time} [${time.time_id}]\n`,
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function reportReservationOption(option, stream) {
|
|
287
|
+
stream.write(`\n${option.name}\n`);
|
|
288
|
+
stream.write(`Option ID: ${option.option_id}\n`);
|
|
289
|
+
stream.write(`Address: ${option.address}\n`);
|
|
290
|
+
if (option.description) stream.write(`Description: ${option.description}\n`);
|
|
291
|
+
if (option.rating !== undefined) {
|
|
292
|
+
stream.write(
|
|
293
|
+
`Rating: ${option.rating}${option.review_count === undefined ? "" : ` from ${option.review_count} reviews`}\n`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (option.cuisines?.length > 0) {
|
|
297
|
+
stream.write(`Cuisines: ${option.cuisines.join(", ")}\n`);
|
|
298
|
+
}
|
|
299
|
+
if (option.price_label) stream.write(`Price: ${option.price_label}\n`);
|
|
300
|
+
if (option.photo_urls?.length > 0) {
|
|
301
|
+
stream.write(`Photos:\n${option.photo_urls.map((url) => ` ${url}`).join("\n")}\n`);
|
|
302
|
+
}
|
|
303
|
+
if (option.match_reasons?.length > 0) {
|
|
304
|
+
stream.write(`Fit: ${option.match_reasons.join("; ")}\n`);
|
|
305
|
+
}
|
|
306
|
+
if (option.unverified?.length > 0) {
|
|
307
|
+
stream.write(`Caveats: ${option.unverified.join("; ")}\n`);
|
|
308
|
+
}
|
|
309
|
+
option.times?.forEach((time, index) => {
|
|
310
|
+
stream.write(`Time ${index + 1}: ${time.date_time} [${time.time_id}]\n`);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizedWords(value) {
|
|
315
|
+
return new Set(
|
|
316
|
+
String(value)
|
|
317
|
+
.toLowerCase()
|
|
318
|
+
.match(/[\p{L}\p{N}]+/gu)
|
|
319
|
+
?.filter((word) => word.length > 2) ?? [],
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function selectReservationOption(input, options, offset) {
|
|
324
|
+
const trimmed = input.trim();
|
|
325
|
+
if (/^[1-9][0-9]*$/u.test(trimmed)) {
|
|
326
|
+
const index = Number(trimmed) - offset - 1;
|
|
327
|
+
return options[index];
|
|
328
|
+
}
|
|
329
|
+
const normalized = trimmed.toLowerCase();
|
|
330
|
+
const exact = options.find(
|
|
331
|
+
(option) => option.name.toLowerCase() === normalized,
|
|
332
|
+
);
|
|
333
|
+
if (exact !== undefined) return exact;
|
|
334
|
+
const contained = options.filter((option) =>
|
|
335
|
+
option.name.toLowerCase().includes(normalized),
|
|
336
|
+
);
|
|
337
|
+
if (contained.length === 1) return contained[0];
|
|
338
|
+
const requested = normalizedWords(trimmed);
|
|
339
|
+
const scored = options
|
|
340
|
+
.map((option) => {
|
|
341
|
+
const observed = normalizedWords(
|
|
342
|
+
[
|
|
343
|
+
option.name,
|
|
344
|
+
option.address,
|
|
345
|
+
...(option.cuisines ?? []),
|
|
346
|
+
option.price_label,
|
|
347
|
+
option.description_excerpt,
|
|
348
|
+
...(option.fit ?? []),
|
|
349
|
+
...(option.caveats ?? []),
|
|
350
|
+
].join(" "),
|
|
351
|
+
);
|
|
352
|
+
return {
|
|
353
|
+
option,
|
|
354
|
+
score: [...requested].filter((word) => observed.has(word)).length,
|
|
355
|
+
};
|
|
356
|
+
})
|
|
357
|
+
.sort((left, right) => right.score - left.score);
|
|
358
|
+
return scored[0]?.score > (scored[1]?.score ?? 0)
|
|
359
|
+
? scored[0].option
|
|
360
|
+
: undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function searchMessage(input) {
|
|
364
|
+
const match = /^(search|refine)\s+(.+)$/iu.exec(input.trim());
|
|
365
|
+
if (match === null) return undefined;
|
|
366
|
+
return `${match[1].toLowerCase() === "search" ? "Search for" : "Refine the search with"} ${match[2].trim()}.`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function browseReservationOptions(reservation, client, dependencies) {
|
|
370
|
+
let page = {
|
|
371
|
+
more_available: reservation.turn.more_available,
|
|
372
|
+
offset: 0,
|
|
373
|
+
options: reservation.turn.options,
|
|
374
|
+
};
|
|
375
|
+
for (;;) {
|
|
376
|
+
const input = String(
|
|
377
|
+
await visiblePrompt(
|
|
378
|
+
"Choose a restaurant number or name, or type more, search <text>, or refine <text>: ",
|
|
379
|
+
dependencies,
|
|
380
|
+
),
|
|
381
|
+
).trim();
|
|
382
|
+
const changedSearch = searchMessage(input);
|
|
383
|
+
if (changedSearch !== undefined) return changedSearch;
|
|
384
|
+
if (input.toLowerCase() === "more") {
|
|
385
|
+
if (page.more_available <= 0) {
|
|
386
|
+
dependencies.stderr.write("There are no more current options.\n");
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const offset = page.offset + page.options.length;
|
|
390
|
+
page = await client.listReservationOptions(reservation.id, {
|
|
391
|
+
limit: 6,
|
|
392
|
+
offset,
|
|
393
|
+
});
|
|
394
|
+
reportReservationOptions(page.options, dependencies.stderr, page.offset);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const selected = selectReservationOption(input, page.options, page.offset);
|
|
398
|
+
if (selected === undefined) {
|
|
399
|
+
dependencies.stderr.write(
|
|
400
|
+
"I could not identify one restaurant. Use its number or name, or refine the search.\n",
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const detail = await client.getReservationOption(
|
|
405
|
+
reservation.id,
|
|
406
|
+
selected.option_id,
|
|
407
|
+
);
|
|
408
|
+
reportReservationOption(detail, dependencies.stderr);
|
|
409
|
+
for (;;) {
|
|
410
|
+
const action = String(
|
|
411
|
+
await visiblePrompt(
|
|
412
|
+
"Choose a time number, or type back, more, search <text>, or refine <text>: ",
|
|
413
|
+
dependencies,
|
|
414
|
+
),
|
|
415
|
+
).trim();
|
|
416
|
+
const refinement = searchMessage(action);
|
|
417
|
+
if (refinement !== undefined) return refinement;
|
|
418
|
+
if (action.toLowerCase() === "back") {
|
|
419
|
+
reportReservationOptions(page.options, dependencies.stderr, page.offset);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
if (action.toLowerCase() === "more") {
|
|
423
|
+
if (page.more_available <= 0) {
|
|
424
|
+
dependencies.stderr.write("There are no more current options.\n");
|
|
425
|
+
} else {
|
|
426
|
+
const offset = page.offset + page.options.length;
|
|
427
|
+
page = await client.listReservationOptions(reservation.id, {
|
|
428
|
+
limit: 6,
|
|
429
|
+
offset,
|
|
430
|
+
});
|
|
431
|
+
reportReservationOptions(
|
|
432
|
+
page.options,
|
|
433
|
+
dependencies.stderr,
|
|
434
|
+
page.offset,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
const time = /^[1-9][0-9]*$/u.test(action)
|
|
440
|
+
? detail.times?.[Number(action) - 1]
|
|
441
|
+
: detail.times?.find(
|
|
442
|
+
(candidate) =>
|
|
443
|
+
candidate.time_id === action ||
|
|
444
|
+
candidate.date_time.toLowerCase().includes(action.toLowerCase()),
|
|
445
|
+
);
|
|
446
|
+
if (time === undefined) {
|
|
447
|
+
dependencies.stderr.write("Choose one of the current time numbers.\n");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
return `Use ${detail.name} with option ID ${detail.option_id} at ${time.date_time} with time ID ${time.time_id}.`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
256
455
|
function reportReservationTurn(reservation, stream) {
|
|
257
456
|
const turn = reservation.turn;
|
|
258
457
|
if (typeof turn?.message === "string") {
|
|
259
458
|
stream.write(`${turn.message}\n`);
|
|
260
459
|
}
|
|
261
|
-
|
|
262
|
-
turn
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
460
|
+
if (turn?.type === "location_options") {
|
|
461
|
+
turn.locations.forEach((location, index) =>
|
|
462
|
+
stream.write(`${index + 1}. ${location.label} [${location.location_id}]\n`),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (turn?.type === "options") {
|
|
466
|
+
reportReservationOptions(turn.options, stream);
|
|
467
|
+
}
|
|
468
|
+
if (turn?.type === "confirmation_required") {
|
|
469
|
+
const summary = turn.confirmation_prompt;
|
|
470
|
+
stream.write(
|
|
471
|
+
`${summary.restaurant}\n${summary.address ?? ""}\n${summary.date_time} for ${summary.party_size ?? "the requested party"}\nOption: ${summary.option_id}\nTime: ${summary.time_id}\n`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function reservationEmailMode(reservation) {
|
|
477
|
+
const mode =
|
|
478
|
+
reservation.turn?.confirmation?.email_delivery_mode ??
|
|
479
|
+
reservation.email_delivery?.mode;
|
|
480
|
+
if (mode === "shopstack_forwarded") return "Shopstack forwarded";
|
|
481
|
+
if (mode === "shopstack_managed") return "Shopstack managed";
|
|
482
|
+
if (mode === "direct") return "Direct";
|
|
483
|
+
return "Not reported";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function reportReservationResult(reservation, fallback, stream) {
|
|
487
|
+
const confirmation = reservation.turn?.confirmation;
|
|
488
|
+
stream.write("Reservation confirmed\n");
|
|
489
|
+
stream.write(
|
|
490
|
+
`Restaurant: ${confirmation?.restaurant ?? fallback?.restaurant ?? "Not reported"}\n`,
|
|
491
|
+
);
|
|
492
|
+
stream.write(
|
|
493
|
+
`Date and time: ${confirmation?.date_time ?? fallback?.date_time ?? "Not reported"}\n`,
|
|
494
|
+
);
|
|
495
|
+
stream.write(
|
|
496
|
+
`Party size: ${confirmation?.party_size ?? fallback?.party_size ?? "Not reported"}\n`,
|
|
497
|
+
);
|
|
498
|
+
stream.write(
|
|
499
|
+
`Confirmation: ${confirmation?.confirmation_number ?? "Not reported"}\n`,
|
|
500
|
+
);
|
|
501
|
+
stream.write(`SMS requested: ${confirmation?.sms_requested === true ? "Yes" : "No"}\n`);
|
|
502
|
+
stream.write(`Email delivery: ${reservationEmailMode(reservation)}\n`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function reportReservationEmail(evidence, stream) {
|
|
506
|
+
stream.write(`Email delivery: ${evidence.email_delivery_mode}\n`);
|
|
507
|
+
if (evidence.messages.length === 0) {
|
|
508
|
+
stream.write("No reservation email has arrived.\n");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
evidence.messages.forEach((message, index) => {
|
|
512
|
+
stream.write(`\n${index + 1}. ${message.subject}\n`);
|
|
513
|
+
stream.write(`Received: ${message.received_at}\n`);
|
|
514
|
+
stream.write(`From: ${message.from}\n`);
|
|
515
|
+
stream.write(`Forwarding: ${message.forwarding.status}\n`);
|
|
516
|
+
if (message.text_body) stream.write(`${message.text_body}\n`);
|
|
517
|
+
else if (message.html_body) stream.write(`${message.html_body}\n`);
|
|
518
|
+
});
|
|
268
519
|
}
|
|
269
520
|
|
|
270
521
|
async function activeClient(dependencies, requiredKind = "user") {
|
|
@@ -520,15 +771,24 @@ export async function runCli(args, supplied = {}) {
|
|
|
520
771
|
}
|
|
521
772
|
|
|
522
773
|
if (group === "reservation" && action === "run") {
|
|
523
|
-
const { options, positional } = parseOptions(
|
|
774
|
+
const { options, positional } = parseOptions(
|
|
775
|
+
rest,
|
|
776
|
+
new Set(["file", "json"]),
|
|
777
|
+
new Set(["json"]),
|
|
778
|
+
);
|
|
524
779
|
if (positional.length > 0) {
|
|
525
780
|
throw new Error("Unexpected reservation argument.");
|
|
526
781
|
}
|
|
527
782
|
const { client } = await activeClient(dependencies, "user");
|
|
528
783
|
const request = await dependencies.readJsonFile(required(options, "file"));
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
784
|
+
let lastConfirmationPrompt;
|
|
785
|
+
let result = await client.runReservation(request, {
|
|
786
|
+
onTurn: (reservation) => {
|
|
787
|
+
if (reservation.turn?.type === "confirmation_required") {
|
|
788
|
+
lastConfirmationPrompt = reservation.turn.confirmation_prompt;
|
|
789
|
+
}
|
|
790
|
+
reportReservationTurn(reservation, dependencies.stderr);
|
|
791
|
+
},
|
|
532
792
|
respond: async (reservation) => {
|
|
533
793
|
if (reservation.status === "confirmation_required") {
|
|
534
794
|
const accepted = dependencies.confirm
|
|
@@ -546,10 +806,47 @@ export async function runCli(args, supplied = {}) {
|
|
|
546
806
|
? "Yes, confirm this exact booking."
|
|
547
807
|
: "No, do not book this reservation.";
|
|
548
808
|
}
|
|
809
|
+
if (reservation.turn?.type === "options") {
|
|
810
|
+
return browseReservationOptions(reservation, client, dependencies);
|
|
811
|
+
}
|
|
549
812
|
return String(await visiblePrompt("Your reply: ", dependencies)).trim();
|
|
550
813
|
},
|
|
551
814
|
});
|
|
552
|
-
|
|
815
|
+
const completedBooking = result.status === "confirmed";
|
|
816
|
+
if (completedBooking) {
|
|
817
|
+
if (options.json !== true) {
|
|
818
|
+
reportReservationResult(
|
|
819
|
+
result,
|
|
820
|
+
lastConfirmationPrompt,
|
|
821
|
+
dependencies.stdout,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (
|
|
825
|
+
isAffirmative(
|
|
826
|
+
await visiblePrompt(
|
|
827
|
+
"Cancel this reservation now? [y/N] ",
|
|
828
|
+
dependencies,
|
|
829
|
+
),
|
|
830
|
+
)
|
|
831
|
+
) {
|
|
832
|
+
result = await client.cancelReservation(result.id, result.revision);
|
|
833
|
+
if (options.json !== true) {
|
|
834
|
+
if (result.status === "cancelled") {
|
|
835
|
+
dependencies.stdout.write("Reservation cancelled\n");
|
|
836
|
+
dependencies.stdout.write(
|
|
837
|
+
`Confirmation: ${result.turn?.cancellation?.confirmation_number ?? result.turn?.confirmation?.confirmation_number ?? "Not reported"}\n`,
|
|
838
|
+
);
|
|
839
|
+
} else {
|
|
840
|
+
dependencies.stdout.write(
|
|
841
|
+
`Cancellation status: ${result.status}\n`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (options.json === true || !completedBooking) {
|
|
848
|
+
writeJson(dependencies.stdout, result);
|
|
849
|
+
}
|
|
553
850
|
return;
|
|
554
851
|
}
|
|
555
852
|
|
|
@@ -560,6 +857,22 @@ export async function runCli(args, supplied = {}) {
|
|
|
560
857
|
return;
|
|
561
858
|
}
|
|
562
859
|
|
|
860
|
+
if (group === "reservation" && action === "email") {
|
|
861
|
+
const { options, positional } = parseOptions(
|
|
862
|
+
rest,
|
|
863
|
+
new Set(["json"]),
|
|
864
|
+
new Set(["json"]),
|
|
865
|
+
);
|
|
866
|
+
if (positional.length !== 1) {
|
|
867
|
+
throw new Error("A reservation ID is required.");
|
|
868
|
+
}
|
|
869
|
+
const { client } = await activeClient(dependencies, "user");
|
|
870
|
+
const evidence = await client.getReservationEmail(positional[0]);
|
|
871
|
+
if (options.json === true) writeJson(dependencies.stdout, evidence);
|
|
872
|
+
else reportReservationEmail(evidence, dependencies.stdout);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
563
876
|
if (group === "reservation" && action === "options") {
|
|
564
877
|
const { options, positional } = parseOptions(
|
|
565
878
|
rest,
|
|
@@ -676,9 +989,9 @@ export async function runCli(args, supplied = {}) {
|
|
|
676
989
|
);
|
|
677
990
|
return /^y(?:es)?$/iu.test(answer.trim());
|
|
678
991
|
},
|
|
679
|
-
message: (checkout) =>
|
|
992
|
+
message: (checkout, question) =>
|
|
680
993
|
visiblePrompt(
|
|
681
|
-
`${checkout.activity || "Shopstack needs more information"}: `,
|
|
994
|
+
`${question?.content || checkout.activity || "Shopstack needs more information"}: `,
|
|
682
995
|
dependencies,
|
|
683
996
|
),
|
|
684
997
|
paymentDetails: async () => ({
|
package/src/client.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface CreateCheckoutRequest {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface ReservationDiner {
|
|
42
|
+
/** ISO 3166-1 alpha-2 country for phone_number, not the restaurant location. */
|
|
42
43
|
country: string;
|
|
43
44
|
/** Optional booking contact address. If omitted, Shopstack creates a temporary inbox. */
|
|
44
45
|
email?: string;
|
|
@@ -115,6 +116,18 @@ export interface PaymentApproval {
|
|
|
115
116
|
expires_at: string;
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
export interface CheckoutQuestion {
|
|
120
|
+
id: string;
|
|
121
|
+
content: string;
|
|
122
|
+
sequence: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CheckoutMessage extends CheckoutQuestion {
|
|
126
|
+
checkout_id: string;
|
|
127
|
+
role: "user" | "assistant";
|
|
128
|
+
created_at: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
export interface Checkout {
|
|
119
132
|
id: string;
|
|
120
133
|
status:
|
|
@@ -172,11 +185,26 @@ export interface Reservation {
|
|
|
172
185
|
updated_at: string;
|
|
173
186
|
/** Search-session expiry; it does not end the life of a confirmed booking. */
|
|
174
187
|
expires_at: string;
|
|
188
|
+
/** Present only when Shopstack forwards provider email from its reservation inbox. */
|
|
189
|
+
email_delivery?: {
|
|
190
|
+
mode: "shopstack_forwarded";
|
|
191
|
+
status: "pending" | "sent" | "failed";
|
|
192
|
+
};
|
|
175
193
|
/** Last retained time for reading and managing the confirmed booking. */
|
|
176
194
|
}
|
|
177
195
|
|
|
178
196
|
export type ReservationTurn =
|
|
179
|
-
| {
|
|
197
|
+
| {
|
|
198
|
+
type: "question" | "message";
|
|
199
|
+
message: string;
|
|
200
|
+
error_code?:
|
|
201
|
+
| "BOOKING_REJECTED"
|
|
202
|
+
| "BOOKING_UNKNOWN"
|
|
203
|
+
| "DINER_ALREADY_REGISTERED"
|
|
204
|
+
| "DINER_PHONE_INVALID"
|
|
205
|
+
| "DINER_PROFILE_INVALID"
|
|
206
|
+
| "UPSTREAM_UNAVAILABLE";
|
|
207
|
+
}
|
|
180
208
|
| {
|
|
181
209
|
type: "location_options";
|
|
182
210
|
message: string;
|
|
@@ -208,8 +236,13 @@ export type ReservationTurn =
|
|
|
208
236
|
confirmation_number: string;
|
|
209
237
|
restaurant: string;
|
|
210
238
|
date_time: string;
|
|
239
|
+
email_delivery_mode:
|
|
240
|
+
| "direct"
|
|
241
|
+
| "shopstack_forwarded"
|
|
242
|
+
| "shopstack_managed";
|
|
211
243
|
party_size: number;
|
|
212
244
|
status: string;
|
|
245
|
+
sms_requested: true;
|
|
213
246
|
};
|
|
214
247
|
}
|
|
215
248
|
| {
|
|
@@ -264,6 +297,24 @@ export interface ReservationOptionPage {
|
|
|
264
297
|
total_available: number;
|
|
265
298
|
}
|
|
266
299
|
|
|
300
|
+
export interface ReservationEmailEvidence {
|
|
301
|
+
email_delivery_mode: "direct" | "shopstack_forwarded" | "shopstack_managed";
|
|
302
|
+
messages: {
|
|
303
|
+
forwarding: {
|
|
304
|
+
attempt_count: number;
|
|
305
|
+
last_error_category?: string;
|
|
306
|
+
sent_at?: string;
|
|
307
|
+
status: "failed" | "not_applicable" | "pending" | "sent";
|
|
308
|
+
};
|
|
309
|
+
from: string;
|
|
310
|
+
html_body?: string;
|
|
311
|
+
received_at: string;
|
|
312
|
+
subject: string;
|
|
313
|
+
text_body?: string;
|
|
314
|
+
}[];
|
|
315
|
+
reservation_id: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
267
318
|
export interface RunReservationOptions {
|
|
268
319
|
idempotencyKey?: string;
|
|
269
320
|
onTurn?(reservation: Reservation): void | Promise<void>;
|
|
@@ -287,6 +338,7 @@ export interface RunCheckoutOptions {
|
|
|
287
338
|
): boolean | Promise<boolean | undefined> | undefined;
|
|
288
339
|
message?(
|
|
289
340
|
checkout: Checkout,
|
|
341
|
+
question: CheckoutQuestion,
|
|
290
342
|
): string | Promise<string | undefined> | undefined;
|
|
291
343
|
}
|
|
292
344
|
|
|
@@ -415,6 +467,7 @@ export class ShopstackClient {
|
|
|
415
467
|
options?: ReservationMutationOptions,
|
|
416
468
|
): Promise<Reservation>;
|
|
417
469
|
getReservation(reservationId: string): Promise<Reservation>;
|
|
470
|
+
getReservationEmail(reservationId: string): Promise<ReservationEmailEvidence>;
|
|
418
471
|
listReservationOptions(
|
|
419
472
|
reservationId: string,
|
|
420
473
|
options?: { limit?: number; offset?: number },
|
|
@@ -470,7 +523,17 @@ export class ShopstackClient {
|
|
|
470
523
|
content: string,
|
|
471
524
|
options?: { idempotencyKey?: string },
|
|
472
525
|
): Promise<Record<string, unknown>>;
|
|
473
|
-
listMessages(
|
|
526
|
+
listMessages(
|
|
527
|
+
checkoutId: string,
|
|
528
|
+
options?: { after?: string; limit?: number },
|
|
529
|
+
): Promise<{
|
|
530
|
+
data: CheckoutMessage[];
|
|
531
|
+
has_more: boolean;
|
|
532
|
+
next_cursor: string | null;
|
|
533
|
+
}>;
|
|
534
|
+
getCheckoutQuestion(
|
|
535
|
+
checkout: Checkout,
|
|
536
|
+
): Promise<CheckoutQuestion | undefined>;
|
|
474
537
|
listEvents(checkoutId: string): Promise<Record<string, unknown>>;
|
|
475
538
|
providePaymentDetails(
|
|
476
539
|
checkoutId: string,
|
package/src/client.js
CHANGED
|
@@ -384,6 +384,12 @@ export class ShopstackClient {
|
|
|
384
384
|
return this.request(`/reservations/${encodeURIComponent(reservationId)}`);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
getReservationEmail(reservationId) {
|
|
388
|
+
return this.request(
|
|
389
|
+
`/reservations/${encodeURIComponent(reservationId)}/email`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
387
393
|
listReservationOptions(reservationId, { limit = 6, offset = 0 } = {}) {
|
|
388
394
|
const query = new URLSearchParams({
|
|
389
395
|
offset: String(offset),
|
|
@@ -525,8 +531,47 @@ export class ShopstackClient {
|
|
|
525
531
|
);
|
|
526
532
|
}
|
|
527
533
|
|
|
528
|
-
listMessages(checkoutId) {
|
|
529
|
-
|
|
534
|
+
listMessages(checkoutId, { after, limit } = {}) {
|
|
535
|
+
const query = new URLSearchParams();
|
|
536
|
+
if (after !== undefined) query.set("after", after);
|
|
537
|
+
if (limit !== undefined) query.set("limit", String(limit));
|
|
538
|
+
const suffix = query.size ? `?${query}` : "";
|
|
539
|
+
return this.request(
|
|
540
|
+
`/checkout/${encodeURIComponent(checkoutId)}/messages${suffix}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async getCheckoutQuestion(checkout) {
|
|
545
|
+
if (
|
|
546
|
+
checkout.status !== "help_required" ||
|
|
547
|
+
checkout.required_input !== undefined
|
|
548
|
+
)
|
|
549
|
+
return undefined;
|
|
550
|
+
let latest;
|
|
551
|
+
let after;
|
|
552
|
+
const cursors = new Set();
|
|
553
|
+
do {
|
|
554
|
+
const page = await this.listMessages(checkout.id, { after, limit: 100 });
|
|
555
|
+
latest = page.data.at(-1) ?? latest;
|
|
556
|
+
if (!page.has_more) break;
|
|
557
|
+
if (
|
|
558
|
+
typeof page.next_cursor !== "string" ||
|
|
559
|
+
!page.next_cursor ||
|
|
560
|
+
cursors.has(page.next_cursor)
|
|
561
|
+
)
|
|
562
|
+
throw new Error("Checkout message pagination did not advance.");
|
|
563
|
+
after = page.next_cursor;
|
|
564
|
+
cursors.add(after);
|
|
565
|
+
} while (true);
|
|
566
|
+
// A posted answer can arrive before the runner changes help_required.
|
|
567
|
+
// Never ask an already answered question again during that interval.
|
|
568
|
+
if (latest?.role !== "assistant" || latest.checkout_id !== checkout.id)
|
|
569
|
+
return undefined;
|
|
570
|
+
return {
|
|
571
|
+
id: latest.id,
|
|
572
|
+
content: latest.content,
|
|
573
|
+
sequence: latest.sequence,
|
|
574
|
+
};
|
|
530
575
|
}
|
|
531
576
|
|
|
532
577
|
listEvents(checkoutId) {
|
|
@@ -640,8 +685,16 @@ export class ShopstackClient {
|
|
|
640
685
|
handledMessageRevision !== checkout.revision
|
|
641
686
|
) {
|
|
642
687
|
if (typeof options.message !== "function") return checkout;
|
|
688
|
+
const question = await this.getCheckoutQuestion(checkout);
|
|
689
|
+
if (question === undefined) {
|
|
690
|
+
await monitor.wait(
|
|
691
|
+
checkout.presentation_revision ?? 0,
|
|
692
|
+
timeoutMs - (Date.now() - startedAt),
|
|
693
|
+
);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
643
696
|
handledMessageRevision = checkout.revision;
|
|
644
|
-
const content = await options.message(checkout);
|
|
697
|
+
const content = await options.message(checkout, question);
|
|
645
698
|
if (typeof content !== "string" || content.length === 0)
|
|
646
699
|
return checkout;
|
|
647
700
|
await this.sendMessage(checkout.id, content, {
|