runline 0.30.0 → 0.31.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 (35) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/plugins/_shared/authedFetch.js +33 -0
  4. package/dist/plugins/_shared/provider.js +80 -0
  5. package/dist/plugins/_shared/tokenRefresh.js +5 -3
  6. package/dist/plugins/gett/src/index.js +358 -0
  7. package/dist/plugins/gett/src/places.js +140 -0
  8. package/dist/plugins/gett/src/rides.js +144 -0
  9. package/dist/plugins/gett/src/session.js +242 -0
  10. package/dist/plugins/gett/src/shared.js +180 -0
  11. package/dist/plugins/linear/src/attachments.js +2 -2
  12. package/dist/plugins/linear/src/cycles.js +6 -6
  13. package/dist/plugins/linear/src/index.js +2 -2
  14. package/dist/plugins/linear/src/initiatives.js +6 -6
  15. package/dist/plugins/linear/src/issues.js +9 -9
  16. package/dist/plugins/linear/src/labels.js +6 -6
  17. package/dist/plugins/linear/src/organization.js +2 -2
  18. package/dist/plugins/linear/src/projects.js +12 -12
  19. package/dist/plugins/linear/src/shared.js +148 -20
  20. package/dist/plugins/linear/src/states.js +3 -3
  21. package/dist/plugins/linear/src/teams.js +6 -5
  22. package/dist/plugins/linear/src/users.js +3 -3
  23. package/dist/plugins/linear/src/views.js +10 -8
  24. package/dist/plugins/linear/src/webhooks.js +5 -5
  25. package/dist/plugins/openai/src/index.js +28 -9
  26. package/dist/plugins/typesafe/src/guide.js +172 -0
  27. package/dist/plugins/typesafe/src/index.js +169 -0
  28. package/dist/plugins/typesafe/src/questions.js +180 -0
  29. package/dist/plugins/typesafe/src/shared.js +166 -0
  30. package/dist/plugins/wolt/src/account.js +427 -0
  31. package/dist/plugins/wolt/src/catalog.js +166 -0
  32. package/dist/plugins/wolt/src/index.js +526 -0
  33. package/dist/plugins/wolt/src/orders.js +349 -0
  34. package/dist/plugins/wolt/src/shared.js +218 -0
  35. package/package.json +1 -1
@@ -0,0 +1,526 @@
1
+ import * as t from "typebox";
2
+ import { discoverPaymentMethod, listAddresses, loginMethods, redeemLink, requestEmailCode, requestSmsCode, status, submitCode, submitConfirmation, whoami, } from "./account.js";
3
+ import { item, menu, nearby, search, venue } from "./catalog.js";
4
+ import { cancelOrder, estimate, orderStatus, planOrder, previewOf, purchase, quoteFor, } from "./orders.js";
5
+ import { accessToken, cfgOf, DEF_LAT, DEF_LON, num, } from "./shared.js";
6
+ /**
7
+ * wolt — Wolt (food and grocery delivery, Israel and global) consumer connector.
8
+ *
9
+ * The catalogue is ANONYMOUS: search, venues, menus, and price estimates all
10
+ * work on a connection that has never been logged in. Ordering is authenticated
11
+ * and rides the mobile app's surface.
12
+ *
13
+ * Login reaches one account through several doors — an emailed code, an emailed
14
+ * magic link, or a phone merged with a social account — and every door that
15
+ * SENDS something needs a fresh passive hCaptcha token minted in a real browser
16
+ * on a wolt.com origin. Redeeming a link is the only send-free step.
17
+ *
18
+ * Money: `order.create` charges the account's saved card and sends real food to
19
+ * a real address, so consent is bound to a total rather than to an intent. A
20
+ * call without `confirm` returns a priced preview and a `quote` fingerprinting
21
+ * the venue, every line, the address, and the payable amount; placing needs
22
+ * `allowOrdering` on the connection, `confirm: true`, and that same quote still
23
+ * pricing identically. A total that moved refuses and reports the new one.
24
+ *
25
+ * Amounts are integer MINOR UNITS throughout: 1800 is ₪18.00.
26
+ */
27
+ const STRICT = { additionalProperties: false };
28
+ const latSchema = t.Number({
29
+ minimum: -90,
30
+ maximum: 90,
31
+ description: "Latitude. Defaults to the connection's home location.",
32
+ });
33
+ const lonSchema = t.Number({
34
+ minimum: -180,
35
+ maximum: 180,
36
+ description: "Longitude. Defaults to the connection's home location.",
37
+ });
38
+ const slugSchema = t.String({
39
+ minLength: 1,
40
+ maxLength: 200,
41
+ description: "Venue slug, from venue.search or venue.listNearby.",
42
+ });
43
+ const orderIdSchema = t.String({
44
+ minLength: 1,
45
+ maxLength: 128,
46
+ description: "Order id returned by order.create.",
47
+ });
48
+ const captchaSchema = (what) => t.String({
49
+ minLength: 1,
50
+ maxLength: 4000,
51
+ description: `Fresh h-captcha-response for ${what}, minted in a real browser on a wolt.com origin. Single use.`,
52
+ });
53
+ /**
54
+ * A cart, typed so the shape is visible to whoever is building one and checked
55
+ * before any of it reaches a payment.
56
+ */
57
+ const cartSchema = t.Array(t.Object({
58
+ id: t.String({
59
+ minLength: 1,
60
+ maxLength: 128,
61
+ description: "Menu item id, from menu.get or item.get.",
62
+ }),
63
+ count: t.Optional(t.Integer({
64
+ minimum: 1,
65
+ maximum: 99,
66
+ description: "How many. Default 1.",
67
+ })),
68
+ options: t.Optional(t.Array(t.Object({
69
+ id: t.String({
70
+ minLength: 1,
71
+ description: "Option group id from the item's `options[].id`. Groups with required:true must be present.",
72
+ }),
73
+ values: t.Array(t.Object({
74
+ id: t.String({
75
+ minLength: 1,
76
+ description: "Chosen value id from the group's `values[].id`.",
77
+ }),
78
+ count: t.Optional(t.Integer({
79
+ minimum: 1,
80
+ maximum: 99,
81
+ description: "Default 1.",
82
+ })),
83
+ }, STRICT), { minItems: 1, description: "The choices made in this group." }),
84
+ }, STRICT), { description: "Option groups for this line, priced from the menu." })),
85
+ }, STRICT), {
86
+ minItems: 1,
87
+ maxItems: 100,
88
+ description: "The cart. Every id is validated against the venue's live assortment before anything is priced, so a stale menu fails loudly instead of ordering the wrong thing.",
89
+ });
90
+ const tipSchema = t.Integer({
91
+ minimum: 0,
92
+ description: "Courier tip in minor units (100 = ₪1.00). Default 0.",
93
+ });
94
+ function coords(input, ctx) {
95
+ const cfg = cfgOf(ctx);
96
+ return {
97
+ lat: num(input.lat ?? cfg.defaultLat, DEF_LAT),
98
+ lon: num(input.lon ?? cfg.defaultLon, DEF_LON),
99
+ };
100
+ }
101
+ export default function wolt(rl) {
102
+ rl.setName("wolt");
103
+ rl.setVersion("0.1.0");
104
+ rl.setConnectionSchema(t.Object({
105
+ refreshToken: t.Optional(t.String({
106
+ env: "WOLT_REFRESH_TOKEN",
107
+ description: "Refresh JWT from an owner login. Rotates on every grant and is rewritten here. Store only in secrets.",
108
+ })),
109
+ deviceToken: t.Optional(t.String({
110
+ env: "WOLT_DEVICE_TOKEN",
111
+ description: "Device token bound to the session. Generated if absent.",
112
+ })),
113
+ visitorId: t.Optional(t.String({
114
+ env: "WOLT_VISITOR_ID",
115
+ description: "x-wolt-visitor-id. Generated if absent.",
116
+ })),
117
+ ravelinDeviceId: t.Optional(t.String({
118
+ env: "WOLT_RAVELIN_DEVICE_ID",
119
+ description: "Ravelin fraud fingerprint (rvnand-6-<64 hex>). Generated if absent; changing it can fail payments.",
120
+ })),
121
+ paymentMethodId: t.Optional(t.String({
122
+ env: "WOLT_PAYMENT_METHOD_ID",
123
+ description: "Saved card charged for orders. Discovered automatically during login.",
124
+ })),
125
+ defaultLat: t.Optional(t.String({
126
+ env: "WOLT_DEFAULT_LAT",
127
+ description: "Home latitude for reads that carry none (e.g. 32.0853).",
128
+ })),
129
+ defaultLon: t.Optional(t.String({
130
+ env: "WOLT_DEFAULT_LON",
131
+ description: "Home longitude for reads that carry none (e.g. 34.7818).",
132
+ })),
133
+ allowOrdering: t.Optional(t.Boolean({
134
+ default: false,
135
+ description: "Master switch for order.create and order.cancel. Off leaves the connection read-only. Even on, order.create still needs confirm:true and a matching quote.",
136
+ })),
137
+ }));
138
+ /* ---------------------------------------------------------------- */
139
+ /* venue / menu / item — anonymous catalogue */
140
+ /* ---------------------------------------------------------------- */
141
+ rl.registerAction("venue.search", {
142
+ access: "read",
143
+ description: "Search Wolt for venues — restaurants, groceries, shops — by name or cuisine near a location. Returns slugs, ratings, delivery price and ETA. Anonymous: works with no login. Hebrew works.",
144
+ inputSchema: t.Object({
145
+ query: t.String({
146
+ minLength: 1,
147
+ maxLength: 200,
148
+ description: "Search text, e.g. 'sushi' or 'pharmacy'.",
149
+ }),
150
+ lat: t.Optional(latSchema),
151
+ lon: t.Optional(lonSchema),
152
+ limit: t.Optional(t.Integer({
153
+ minimum: 1,
154
+ maximum: 100,
155
+ description: "Max venues. Default 20.",
156
+ })),
157
+ }, STRICT),
158
+ async execute(input, ctx) {
159
+ const p = input;
160
+ const { lat, lon } = coords(p, ctx);
161
+ return search(String(p.query), lat, lon, num(p.limit, 20));
162
+ },
163
+ });
164
+ rl.registerAction("venue.listNearby", {
165
+ access: "read",
166
+ description: "The Wolt city front page for a location: venues near you, optionally only the ones open right now. Anonymous.",
167
+ inputSchema: t.Object({
168
+ lat: t.Optional(latSchema),
169
+ lon: t.Optional(lonSchema),
170
+ limit: t.Optional(t.Integer({
171
+ minimum: 1,
172
+ maximum: 100,
173
+ description: "Max venues. Default 30.",
174
+ })),
175
+ open_only: t.Optional(t.Boolean({ description: "Keep only venues currently open." })),
176
+ }, STRICT),
177
+ async execute(input, ctx) {
178
+ const p = (input ?? {});
179
+ const { lat, lon } = coords(p, ctx);
180
+ return nearby(lat, lon, num(p.limit, 30), p.open_only === true);
181
+ },
182
+ });
183
+ rl.registerAction("venue.get", {
184
+ access: "read",
185
+ description: "One venue in full: address, currency, whether it is open, ETA, delivery fee, order minimum, and the opening and delivery schedules. Anonymous.",
186
+ inputSchema: t.Object({
187
+ slug: slugSchema,
188
+ lat: t.Optional(latSchema),
189
+ lon: t.Optional(lonSchema),
190
+ }, STRICT),
191
+ async execute(input, ctx) {
192
+ const p = input;
193
+ const { lat, lon } = coords(p, ctx);
194
+ return venue(String(p.slug), lat, lon);
195
+ },
196
+ });
197
+ rl.registerAction("menu.get", {
198
+ access: "read",
199
+ description: "A venue's menu: categories and items with prices in minor units, stock, and option groups carrying their required flag, their choices and each choice's price — which is what order.create needs to build a cart. Anonymous.",
200
+ inputSchema: t.Object({
201
+ slug: slugSchema,
202
+ q: t.Optional(t.String({
203
+ minLength: 1,
204
+ maxLength: 200,
205
+ description: "Keep only items whose name or description matches.",
206
+ })),
207
+ limit: t.Optional(t.Integer({
208
+ minimum: 0,
209
+ maximum: 500,
210
+ description: "Max items per category. 0 or omitted returns all.",
211
+ })),
212
+ lang: t.Optional(t.String({
213
+ minLength: 2,
214
+ maxLength: 8,
215
+ description: "Assortment language. Default 'en'.",
216
+ })),
217
+ }, STRICT),
218
+ async execute(input) {
219
+ const p = input;
220
+ return menu(String(p.slug), p.q ? String(p.q) : "", num(p.limit, 0), p.lang ? String(p.lang) : "en");
221
+ },
222
+ });
223
+ rl.registerAction("item.get", {
224
+ access: "read",
225
+ description: "One menu item in full — price, stock, and every option group with its choices. Anonymous.",
226
+ inputSchema: t.Object({
227
+ slug: slugSchema,
228
+ item_id: t.String({
229
+ minLength: 1,
230
+ maxLength: 128,
231
+ description: "Item id, from menu.get.",
232
+ }),
233
+ }, STRICT),
234
+ async execute(input) {
235
+ const p = input;
236
+ return item(String(p.slug), String(p.item_id));
237
+ },
238
+ });
239
+ /* ---------------------------------------------------------------- */
240
+ /* account — the owner login and the session it carries */
241
+ /* ---------------------------------------------------------------- */
242
+ rl.registerAction("account.requestEmailCode", {
243
+ access: "write",
244
+ description: "Email door, step 1: ask Wolt to email a login code and link. Needs a fresh hCaptcha token. Returns sent:false with reason 'not_registered' when no account exists for the address — use the phone door instead. Follow with account.submitCode, or account.redeemLink for the link.",
245
+ inputSchema: t.Object({
246
+ email: t.String({
247
+ minLength: 3,
248
+ maxLength: 320,
249
+ description: "The account's email address.",
250
+ }),
251
+ captcha: t.Optional(captchaSchema("the email_login send")),
252
+ }, STRICT),
253
+ async execute(input, ctx) {
254
+ const p = input;
255
+ return requestEmailCode(ctx, p.email, p.captcha);
256
+ },
257
+ });
258
+ rl.registerAction("account.redeemLink", {
259
+ access: "write",
260
+ description: "Redeem an emailed magic link and connect the account. The only login step that sends nothing and needs no captcha. Accepts the whole URL or the bare token; links expire within minutes and are single use.",
261
+ inputSchema: t.Object({
262
+ link: t.String({
263
+ minLength: 8,
264
+ maxLength: 4000,
265
+ description: "The full login URL from the email, or its bare token.",
266
+ }),
267
+ }, STRICT),
268
+ async execute(input, ctx) {
269
+ const { link } = input;
270
+ return redeemLink(ctx, link);
271
+ },
272
+ });
273
+ rl.registerAction("account.loginMethods", {
274
+ access: "read",
275
+ description: "Ask Wolt which doors a phone number can log in through, before committing to one. Sends no code and no email.",
276
+ inputSchema: t.Object({
277
+ phone: t.String({
278
+ minLength: 6,
279
+ maxLength: 32,
280
+ description: "Account phone, Israeli local or international digits.",
281
+ }),
282
+ }, STRICT),
283
+ async execute(input, ctx) {
284
+ const { phone } = input;
285
+ return loginMethods(ctx, phone);
286
+ },
287
+ });
288
+ rl.registerAction("account.requestSmsCode", {
289
+ access: "write",
290
+ description: "Phone door, step 1: ask Wolt to text (or WhatsApp) a login code. This is the door that reaches Google and other social accounts, which is what emailToken is for — a magic-link token from any mailbox proves the email half of the merge. Needs fresh hCaptcha tokens for each send. Follow with account.submitCode.",
291
+ inputSchema: t.Object({
292
+ phone: t.String({
293
+ minLength: 6,
294
+ maxLength: 32,
295
+ description: "Account phone, Israeli local or international digits.",
296
+ }),
297
+ emailToken: t.Optional(t.String({
298
+ minLength: 8,
299
+ maxLength: 4000,
300
+ description: "Magic-link URL or token from an email_login, unlocking the phone-merge path for social accounts.",
301
+ })),
302
+ email: t.Optional(t.String({
303
+ minLength: 3,
304
+ maxLength: 320,
305
+ description: "The address that emailToken was sent to, when it differs from the stored one.",
306
+ })),
307
+ vnCaptcha: t.Optional(captchaSchema("validate-number, in the merge path")),
308
+ smsCaptcha: t.Optional(captchaSchema("the SMS send")),
309
+ viaWhatsapp: t.Optional(t.Boolean({
310
+ description: "Deliver the code over WhatsApp instead of SMS.",
311
+ })),
312
+ }, STRICT),
313
+ async execute(input, ctx) {
314
+ return requestSmsCode(ctx, input);
315
+ },
316
+ });
317
+ rl.registerAction("account.submitCode", {
318
+ access: "write",
319
+ description: "Login step 2: submit the code that was emailed or texted. Connects the account, or reports needs_confirmation:true when Wolt escalates to a second factor — then use account.submitConfirmation.",
320
+ inputSchema: t.Object({
321
+ code: t.String({
322
+ minLength: 3,
323
+ maxLength: 12,
324
+ description: "The one-time code from the email or SMS.",
325
+ }),
326
+ }, STRICT),
327
+ async execute(input, ctx) {
328
+ const { code } = input;
329
+ return submitCode(ctx, code);
330
+ },
331
+ });
332
+ rl.registerAction("account.submitConfirmation", {
333
+ access: "write",
334
+ description: "Login step 3, only when account.submitCode reported needs_confirmation: submit the code Wolt emailed as a second factor. On success the session is connected.",
335
+ inputSchema: t.Object({
336
+ code: t.String({
337
+ minLength: 3,
338
+ maxLength: 12,
339
+ description: "The confirmation code from the email.",
340
+ }),
341
+ }, STRICT),
342
+ async execute(input, ctx) {
343
+ const { code } = input;
344
+ return submitConfirmation(ctx, code);
345
+ },
346
+ });
347
+ rl.registerAction("account.status", {
348
+ access: "read",
349
+ description: "What the stored session has and still needs, read locally with no network call — so it answers even when the session is broken. Reports whether ordering is enabled and whether a login is half-finished.",
350
+ inputSchema: t.Object({}, STRICT),
351
+ async execute(_input, ctx) {
352
+ return status(ctx);
353
+ },
354
+ });
355
+ rl.registerAction("account.refresh", {
356
+ access: "write",
357
+ description: "Force a token refresh. Wolt rotates its refresh token on every grant, so an untouched session eventually lapses; a recurring 'call wolt.account.refresh()' every ~12h keeps it alive. Also the cheapest proof that stored credentials still work.",
358
+ inputSchema: t.Object({}, STRICT),
359
+ async execute(_input, ctx) {
360
+ await accessToken(ctx, true);
361
+ const cfg = cfgOf(ctx);
362
+ return {
363
+ refreshed: true,
364
+ access_expires_at: cfg.accessTokenExpiresAt
365
+ ? new Date(cfg.accessTokenExpiresAt).toISOString()
366
+ : null,
367
+ };
368
+ },
369
+ });
370
+ rl.registerAction("account.get", {
371
+ access: "read",
372
+ description: "The live account behind the session: id, name, saved-address count and the card ordering will charge. Hits Wolt, so it also proves the credentials work — account.status answers from local state alone.",
373
+ inputSchema: t.Object({}, STRICT),
374
+ async execute(_input, ctx) {
375
+ return whoami(ctx);
376
+ },
377
+ });
378
+ rl.registerAction("account.findPaymentMethod", {
379
+ access: "write",
380
+ description: "Re-discover the saved card and store it on the connection. Needed only when a card was added after login, since order.create refuses without one.",
381
+ inputSchema: t.Object({}, STRICT),
382
+ async execute(_input, ctx) {
383
+ const id = await discoverPaymentMethod(ctx);
384
+ return {
385
+ payment_method: id,
386
+ note: id
387
+ ? "Stored; ordering can charge this card."
388
+ : "No usable card on the account — add one in the Wolt app.",
389
+ };
390
+ },
391
+ });
392
+ rl.registerAction("address.list", {
393
+ access: "read",
394
+ description: "The account's saved delivery addresses. order.create delivers to the first one unless given an address id.",
395
+ inputSchema: t.Object({}, STRICT),
396
+ async execute(_input, ctx) {
397
+ return listAddresses(ctx);
398
+ },
399
+ });
400
+ /* ---------------------------------------------------------------- */
401
+ /* order — estimating, placing, tracking */
402
+ /* ---------------------------------------------------------------- */
403
+ rl.registerAction("order.estimate", {
404
+ access: "read",
405
+ description: "What a cart would cost at a venue: delivery fee and totals, without a login and without placing anything. An empty cart still prices the delivery fee. Anonymous.",
406
+ inputSchema: t.Object({
407
+ slug: slugSchema,
408
+ items: t.Optional(cartSchema),
409
+ method: t.Optional(t.String({
410
+ minLength: 1,
411
+ maxLength: 40,
412
+ description: "Delivery method. Default 'homedelivery'.",
413
+ })),
414
+ tip: t.Optional(tipSchema),
415
+ lat: t.Optional(latSchema),
416
+ lon: t.Optional(lonSchema),
417
+ }, STRICT),
418
+ async execute(input, ctx) {
419
+ const p = input;
420
+ const { lat, lon } = coords(p, ctx);
421
+ const detail = await venue(String(p.slug), lat, lon);
422
+ return estimate(detail, p.items ?? [], p.method ? String(p.method) : "homedelivery", num(p.tip, 0));
423
+ },
424
+ });
425
+ rl.registerAction("order.create", {
426
+ access: "write",
427
+ description: "Place a REAL, paid Wolt order: this charges the saved card and sends food to a real address. Call it first WITHOUT confirm to get a priced preview — every line, delivery fee, tip, payable total, ETA, and where it is going — plus a `quote`. Read the total and the address back to the person, get an explicit yes, then call again with confirm:true and that exact quote. If the total moved in between, nothing is ordered and the new total comes back instead, because consent was given to a price. Needs allowOrdering on the connection.",
428
+ inputSchema: t.Object({
429
+ slug: slugSchema,
430
+ items: cartSchema,
431
+ address: t.Optional(t.String({
432
+ minLength: 1,
433
+ maxLength: 128,
434
+ description: "Delivery address id from address.list. Defaults to the first saved address.",
435
+ })),
436
+ tip: t.Optional(tipSchema),
437
+ confirm: t.Optional(t.Boolean({
438
+ description: "True only after a person heard the total and the address and said yes. Must be sent with the quote from the preview call.",
439
+ })),
440
+ quote: t.Optional(t.String({
441
+ minLength: 1,
442
+ maxLength: 64,
443
+ description: "The quote from the preview call. Binds this order to the exact total that was approved.",
444
+ })),
445
+ lat: t.Optional(latSchema),
446
+ lon: t.Optional(lonSchema),
447
+ }, STRICT),
448
+ async execute(input, ctx) {
449
+ const p = input;
450
+ const cfg = cfgOf(ctx);
451
+ // A confirmation that ordering will refuse is decided before pricing it.
452
+ if (p.confirm === true && !cfg.allowOrdering)
453
+ throw new Error("wolt order.create: ordering is disabled for this connection (set allowOrdering:true). Refusing to place.");
454
+ const { lat, lon } = coords(p, ctx);
455
+ const slug = String(p.slug);
456
+ const [detail, addresses] = await Promise.all([
457
+ venue(slug, lat, lon),
458
+ listAddresses(ctx),
459
+ ]);
460
+ const plan = await planOrder(ctx, detail, addresses, p.address ? String(p.address) : undefined, slug, p.items, num(p.tip, 0));
461
+ const quote = quoteFor(plan);
462
+ const summary = previewOf(plan);
463
+ if (p.confirm !== true) {
464
+ return {
465
+ requiresConfirmation: true,
466
+ action: "order.create",
467
+ quote,
468
+ summary,
469
+ note: cfg.allowOrdering
470
+ ? "Places a REAL order and charges the saved card. Read the total and the address back to the person, then re-run with confirm:true and this quote."
471
+ : "Ordering is disabled for this connection (allowOrdering is false), so this preview cannot be placed. Enable allowOrdering first.",
472
+ };
473
+ }
474
+ if (p.quote !== quote) {
475
+ return {
476
+ ordered: false,
477
+ reason: p.quote ? "price_changed" : "quote_required",
478
+ quote,
479
+ summary,
480
+ note: p.quote
481
+ ? "The total changed since the quote that was approved, so nothing was ordered. Read the new total back and confirm again with the new quote."
482
+ : "confirm:true must carry the quote from the preview call, so a person can only approve a total they were shown. Nothing was ordered.",
483
+ };
484
+ }
485
+ const placed = await purchase(ctx, plan);
486
+ return {
487
+ ordered: placed.order_id !== null,
488
+ status: placed.order_id !== null ? "placed" : "unconfirmed",
489
+ order_id: placed.order_id,
490
+ summary,
491
+ ...(placed.order_id
492
+ ? { track_with: `order.status({ order_id: "${placed.order_id}" })` }
493
+ : {
494
+ note: "Wolt accepted the request but returned no order id. Check order history in the app before ordering again.",
495
+ }),
496
+ };
497
+ },
498
+ });
499
+ rl.registerAction("order.status", {
500
+ access: "read",
501
+ description: "Track an order — 'where is my food'. Status, ETA, courier position and hand-off code, delivery distance, and how long to wait before polling again.",
502
+ inputSchema: t.Object({ order_id: orderIdSchema }, STRICT),
503
+ async execute(input, ctx) {
504
+ const { order_id } = input;
505
+ return orderStatus(ctx, order_id);
506
+ },
507
+ });
508
+ rl.registerAction("order.cancel", {
509
+ access: "write",
510
+ description: "Cancel an order, which only works in the short window BEFORE the venue accepts it. After that Wolt refuses with error_code 4051 and only support can help — this reports that as cancelled:false rather than throwing. Needs allowOrdering on the connection.",
511
+ inputSchema: t.Object({
512
+ order_id: orderIdSchema,
513
+ reason: t.Optional(t.String({
514
+ minLength: 1,
515
+ maxLength: 100,
516
+ description: "Cancellation reason. Default 'ordered_by_mistake'.",
517
+ })),
518
+ }, STRICT),
519
+ async execute(input, ctx) {
520
+ const { order_id, reason } = input;
521
+ if (!cfgOf(ctx).allowOrdering)
522
+ throw new Error("wolt order.cancel: ordering is disabled for this connection (set allowOrdering:true).");
523
+ return cancelOrder(ctx, order_id, reason ?? "ordered_by_mistake");
524
+ },
525
+ });
526
+ }