lua-cli 3.42.0-beta.1 → 3.42.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import { z } from 'zod';
12
12
  ## Overview
13
13
 
14
14
  Webhooks enable:
15
+
15
16
  - Receive HTTP requests from external services
16
17
  - Validate query parameters, headers, and body
17
18
  - Process events from third-party platforms
@@ -94,11 +95,22 @@ interface LuaWebhookEvent {
94
95
  /** Request body (JSON or raw data) */
95
96
  body?: any;
96
97
 
98
+ /** Exact unparsed request bytes (utf8) — for verifying vendor signatures */
99
+ rawBody?: string;
100
+
97
101
  /** ISO timestamp when the webhook was received */
98
102
  timestamp: string;
99
103
  }
100
104
  ```
101
105
 
106
+ `rawBody` is the request exactly as it arrived on the wire, as a utf8 string.
107
+ Vendor HMAC schemes (Stripe `t=…,v1=…`, GitHub `sha256=…`, Slack `v0:…`) are
108
+ computed over those bytes, and `JSON.stringify(event.body)` does not reproduce
109
+ them (whitespace, key order and escaping differ), so verify against `rawBody`.
110
+ It is present for JSON requests (`''` for an empty body) and absent for
111
+ form-encoded or plain-text bodies and for webhooks fired by platform events.
112
+ Header keys arrive lowercased. See [Verify Signatures](#verify-signatures).
113
+
102
114
  ## Complete Examples
103
115
 
104
116
  ### GitHub Webhook
@@ -113,22 +125,24 @@ export const githubWebhook = new LuaWebhook({
113
125
 
114
126
  headerSchema: z.object({
115
127
  'x-github-event': z.string(),
116
- 'x-hub-signature': z.string()
128
+ 'x-hub-signature': z.string(),
117
129
  }),
118
130
 
119
131
  bodySchema: z.object({
120
132
  repository: z.object({
121
133
  name: z.string(),
122
- full_name: z.string()
134
+ full_name: z.string(),
123
135
  }),
124
136
  pusher: z.object({
125
137
  name: z.string(),
126
- email: z.string()
138
+ email: z.string(),
127
139
  }),
128
- commits: z.array(z.object({
129
- message: z.string(),
130
- url: z.string()
131
- }))
140
+ commits: z.array(
141
+ z.object({
142
+ message: z.string(),
143
+ url: z.string(),
144
+ })
145
+ ),
132
146
  }),
133
147
 
134
148
  execute: async (event) => {
@@ -138,7 +152,7 @@ export const githubWebhook = new LuaWebhook({
138
152
  if (headers['x-github-event'] !== 'push') {
139
153
  return {
140
154
  success: false,
141
- message: 'Only push events are supported'
155
+ message: 'Only push events are supported',
142
156
  };
143
157
  }
144
158
 
@@ -149,25 +163,27 @@ export const githubWebhook = new LuaWebhook({
149
163
  author: body.pusher.name,
150
164
  message: commit.message,
151
165
  url: commit.url,
152
- timestamp: event.timestamp
166
+ timestamp: event.timestamp,
153
167
  });
154
168
  }
155
169
 
156
170
  // Notify relevant user
157
171
  const user = await User.get({ email: body.pusher.email });
158
172
  if (user) {
159
- await user.send([{
160
- type: 'text',
161
- text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`
162
- }]);
173
+ await user.send([
174
+ {
175
+ type: 'text',
176
+ text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`,
177
+ },
178
+ ]);
163
179
  }
164
180
 
165
181
  return {
166
182
  success: true,
167
183
  commits: body.commits.length,
168
- repository: body.repository.name
184
+ repository: body.repository.name,
169
185
  };
170
- }
186
+ },
171
187
  });
172
188
  ```
173
189
 
@@ -182,7 +198,7 @@ export const stripeWebhook = new LuaWebhook({
182
198
  description: 'Handles Stripe payment events',
183
199
 
184
200
  headerSchema: z.object({
185
- 'stripe-signature': z.string()
201
+ 'stripe-signature': z.string(),
186
202
  }),
187
203
 
188
204
  bodySchema: z.object({
@@ -193,9 +209,9 @@ export const stripeWebhook = new LuaWebhook({
193
209
  amount: z.number(),
194
210
  currency: z.string(),
195
211
  customer: z.string().optional(),
196
- metadata: z.record(z.any()).optional()
197
- })
198
- })
212
+ metadata: z.record(z.any()).optional(),
213
+ }),
214
+ }),
199
215
  }),
200
216
 
201
217
  execute: async (event) => {
@@ -214,23 +230,25 @@ export const stripeWebhook = new LuaWebhook({
214
230
  await order.update({
215
231
  paymentId: payment.id,
216
232
  paidAmount: payment.amount / 100,
217
- paidAt: event.timestamp
233
+ paidAt: event.timestamp,
218
234
  });
219
235
 
220
236
  // Notify customer
221
237
  if (payment.customer) {
222
238
  const user = await User.get(payment.customer);
223
- await user.send([{
224
- type: 'text',
225
- text: `✅ Payment received! Your order #${orderId} is being processed.`
226
- }]);
239
+ await user.send([
240
+ {
241
+ type: 'text',
242
+ text: `✅ Payment received! Your order #${orderId} is being processed.`,
243
+ },
244
+ ]);
227
245
  }
228
246
  }
229
247
 
230
248
  return {
231
249
  success: true,
232
250
  event: 'payment_succeeded',
233
- orderId
251
+ orderId,
234
252
  };
235
253
  }
236
254
 
@@ -241,16 +259,18 @@ export const stripeWebhook = new LuaWebhook({
241
259
  // Notify customer of failure
242
260
  if (payment.customer) {
243
261
  const user = await User.get(payment.customer);
244
- await user.send([{
245
- type: 'text',
246
- text: `❌ Payment failed for order #${orderId}. Please try again.`
247
- }]);
262
+ await user.send([
263
+ {
264
+ type: 'text',
265
+ text: `❌ Payment failed for order #${orderId}. Please try again.`,
266
+ },
267
+ ]);
248
268
  }
249
269
 
250
270
  return {
251
271
  success: true,
252
272
  event: 'payment_failed',
253
- orderId
273
+ orderId,
254
274
  };
255
275
  }
256
276
 
@@ -258,10 +278,10 @@ export const stripeWebhook = new LuaWebhook({
258
278
  return {
259
279
  success: true,
260
280
  event: body.type,
261
- message: 'Event received but not handled'
281
+ message: 'Event received but not handled',
262
282
  };
263
283
  }
264
- }
284
+ },
265
285
  });
266
286
  ```
267
287
 
@@ -278,12 +298,14 @@ export const slackWebhook = new LuaWebhook({
278
298
  bodySchema: z.object({
279
299
  type: z.string(),
280
300
  challenge: z.string().optional(),
281
- event: z.object({
282
- type: z.string(),
283
- user: z.string().optional(),
284
- text: z.string().optional(),
285
- channel: z.string()
286
- }).optional()
301
+ event: z
302
+ .object({
303
+ type: z.string(),
304
+ user: z.string().optional(),
305
+ text: z.string().optional(),
306
+ channel: z.string(),
307
+ })
308
+ .optional(),
287
309
  }),
288
310
 
289
311
  execute: async (event) => {
@@ -306,10 +328,9 @@ export const slackWebhook = new LuaWebhook({
306
328
  // Process user message
307
329
  if (slackEvent.type === 'message' && slackEvent.text) {
308
330
  // Generate AI response
309
- const response = await AI.generate(
310
- 'You are a helpful Slack bot assistant.',
311
- [{ type: 'text', text: slackEvent.text }]
312
- );
331
+ const response = await AI.generate('You are a helpful Slack bot assistant.', [
332
+ { type: 'text', text: slackEvent.text },
333
+ ]);
313
334
 
314
335
  // Store interaction
315
336
  await Data.create('slack_messages', {
@@ -317,20 +338,20 @@ export const slackWebhook = new LuaWebhook({
317
338
  channel: slackEvent.channel,
318
339
  message: slackEvent.text,
319
340
  response,
320
- timestamp: event.timestamp
341
+ timestamp: event.timestamp,
321
342
  });
322
343
 
323
344
  // TODO: Send response back to Slack using Slack API
324
345
 
325
346
  return {
326
347
  success: true,
327
- response
348
+ response,
328
349
  };
329
350
  }
330
351
  }
331
352
 
332
353
  return { success: true, message: 'Event received' };
333
- }
354
+ },
334
355
  });
335
356
  ```
336
357
 
@@ -345,7 +366,7 @@ export const contactFormWebhook = new LuaWebhook({
345
366
  description: 'Handles contact form submissions',
346
367
 
347
368
  querySchema: z.object({
348
- source: z.enum(['website', 'mobile_app']).optional()
369
+ source: z.enum(['website', 'mobile_app']).optional(),
349
370
  }),
350
371
 
351
372
  bodySchema: z.object({
@@ -354,7 +375,7 @@ export const contactFormWebhook = new LuaWebhook({
354
375
  phone: z.string().optional(),
355
376
  subject: z.string(),
356
377
  message: z.string().min(10),
357
- consent: z.boolean()
378
+ consent: z.boolean(),
358
379
  }),
359
380
 
360
381
  execute: async (event) => {
@@ -364,7 +385,7 @@ export const contactFormWebhook = new LuaWebhook({
364
385
  if (!body.consent) {
365
386
  return {
366
387
  success: false,
367
- error: 'Consent is required'
388
+ error: 'Consent is required',
368
389
  };
369
390
  }
370
391
 
@@ -377,7 +398,7 @@ export const contactFormWebhook = new LuaWebhook({
377
398
  message: body.message,
378
399
  source: query?.source || 'unknown',
379
400
  submittedAt: event.timestamp,
380
- status: 'new'
401
+ status: 'new',
381
402
  });
382
403
 
383
404
  // Create or update user
@@ -387,22 +408,24 @@ export const contactFormWebhook = new LuaWebhook({
387
408
  await user.update({
388
409
  name: body.name,
389
410
  email: body.email,
390
- phone: body.phone
411
+ phone: body.phone,
391
412
  });
392
413
  }
393
414
 
394
415
  // Send confirmation
395
- await user.send([{
396
- type: 'text',
397
- text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`
398
- }]);
416
+ await user.send([
417
+ {
418
+ type: 'text',
419
+ text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`,
420
+ },
421
+ ]);
399
422
 
400
423
  return {
401
424
  success: true,
402
425
  submissionId: entry.id,
403
- message: 'Form submitted successfully'
426
+ message: 'Form submitted successfully',
404
427
  };
405
- }
428
+ },
406
429
  });
407
430
  ```
408
431
 
@@ -418,17 +441,17 @@ export const apiWebhook = new LuaWebhook({
418
441
 
419
442
  headerSchema: z.object({
420
443
  'x-api-key': z.string(),
421
- 'content-type': z.literal('application/json')
444
+ 'content-type': z.literal('application/json'),
422
445
  }),
423
446
 
424
447
  querySchema: z.object({
425
448
  action: z.enum(['create', 'update', 'delete']),
426
- entity: z.string()
449
+ entity: z.string(),
427
450
  }),
428
451
 
429
452
  bodySchema: z.object({
430
453
  id: z.string(),
431
- data: z.record(z.any())
454
+ data: z.record(z.any()),
432
455
  }),
433
456
 
434
457
  execute: async (event) => {
@@ -439,7 +462,7 @@ export const apiWebhook = new LuaWebhook({
439
462
  if (headers['x-api-key'] !== validApiKey) {
440
463
  return {
441
464
  success: false,
442
- error: 'Invalid API key'
465
+ error: 'Invalid API key',
443
466
  };
444
467
  }
445
468
 
@@ -449,7 +472,7 @@ export const apiWebhook = new LuaWebhook({
449
472
  await Data.create(query.entity, {
450
473
  externalId: body.id,
451
474
  ...body.data,
452
- syncedAt: event.timestamp
475
+ syncedAt: event.timestamp,
453
476
  });
454
477
  break;
455
478
 
@@ -458,7 +481,7 @@ export const apiWebhook = new LuaWebhook({
458
481
  if (entries.data.length > 0) {
459
482
  await Data.update(query.entity, entries.data[0].id, {
460
483
  ...body.data,
461
- syncedAt: event.timestamp
484
+ syncedAt: event.timestamp,
462
485
  });
463
486
  }
464
487
  break;
@@ -475,9 +498,9 @@ export const apiWebhook = new LuaWebhook({
475
498
  success: true,
476
499
  action: query.action,
477
500
  entity: query.entity,
478
- id: body.id
501
+ id: body.id,
479
502
  };
480
- }
503
+ },
481
504
  });
482
505
  ```
483
506
 
@@ -490,6 +513,7 @@ https://api.heylua.ai/webhook/{agentId}/{webhookName}
490
513
  ```
491
514
 
492
515
  Example:
516
+
493
517
  ```
494
518
  https://api.heylua.ai/webhook/agent_123/github-push
495
519
  ```
@@ -551,7 +575,7 @@ export const webhook = new LuaWebhook({
551
575
  description: 'Webhook with error handling',
552
576
 
553
577
  bodySchema: z.object({
554
- data: z.string()
578
+ data: z.string(),
555
579
  }),
556
580
 
557
581
  execute: async (event) => {
@@ -561,7 +585,7 @@ export const webhook = new LuaWebhook({
561
585
 
562
586
  return {
563
587
  success: true,
564
- result
588
+ result,
565
589
  };
566
590
  } catch (error) {
567
591
  // Log error
@@ -569,17 +593,17 @@ export const webhook = new LuaWebhook({
569
593
  webhookName: 'robust-webhook',
570
594
  error: error.message,
571
595
  event: event.body,
572
- timestamp: event.timestamp
596
+ timestamp: event.timestamp,
573
597
  });
574
598
 
575
599
  // Return error response
576
600
  return {
577
601
  success: false,
578
602
  error: error.message,
579
- retry: true // Indicate that the sender should retry
603
+ retry: true, // Indicate that the sender should retry
580
604
  };
581
605
  }
582
- }
606
+ },
583
607
  });
584
608
  ```
585
609
 
@@ -587,32 +611,51 @@ export const webhook = new LuaWebhook({
587
611
 
588
612
  ### Verify Signatures
589
613
 
614
+ Verify a vendor's signature in `execute` against `event.rawBody` — the exact
615
+ bytes the vendor signed — never against a re-serialized `event.body`:
616
+
590
617
  ```typescript
591
618
  import crypto from 'crypto';
619
+ import { LuaWebhook, env } from 'lua-cli';
592
620
 
593
- function verifyGitHubSignature(
594
- body: string,
595
- signature: string,
596
- secret: string
597
- ): boolean {
598
- const hmac = crypto.createHmac('sha256', secret);
599
- const digest = 'sha256=' + hmac.update(body).digest('hex');
600
- return crypto.timingSafeEqual(
601
- Buffer.from(signature),
602
- Buffer.from(digest)
603
- );
621
+ function verifyGitHubSignature(rawBody: string, signature: string, secret: string): boolean {
622
+ const digest = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
623
+ const a = Buffer.from(signature);
624
+ const b = Buffer.from(digest);
625
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
604
626
  }
627
+
628
+ export const githubPush = new LuaWebhook({
629
+ name: 'github-push-verified',
630
+ description: 'GitHub push events, signature-checked',
631
+ execute: async (event) => {
632
+ const signature = event.headers?.['x-hub-signature-256'];
633
+ const secret = env('GITHUB_WEBHOOK_SECRET');
634
+ if (!event.rawBody || !signature || !secret || !verifyGitHubSignature(event.rawBody, signature, secret)) {
635
+ return { success: false, error: 'invalid signature' };
636
+ }
637
+ // Signature OK — event.body is the parsed form of the same bytes.
638
+ return { success: true, ref: event.body.ref };
639
+ },
640
+ });
605
641
  ```
606
642
 
643
+ `lua test webhook` has no wire bytes, so it sets `rawBody` to the compact
644
+ `JSON.stringify(body)` of the test input; pass an explicit one with
645
+ `--input '{"body": {...}, "headers": {...}, "rawBody": "<exact bytes>"}'` to
646
+ exercise a signature check against a captured delivery.
647
+
648
+ To have Lua reject unsigned calls before `execute` runs, use the built-in
649
+ `secret` instead (see [Request Verification](#request-verification)).
650
+
607
651
  ### API Key Validation
608
652
 
609
653
  ```typescript
610
654
  headerSchema: z.object({
611
- 'x-api-key': z.string()
612
- .refine(key => key === process.env.WEBHOOK_API_KEY, {
613
- message: 'Invalid API key'
614
- })
615
- })
655
+ 'x-api-key': z.string().refine((key) => key === process.env.WEBHOOK_API_KEY, {
656
+ message: 'Invalid API key',
657
+ }),
658
+ });
616
659
  ```
617
660
 
618
661
  ## Related APIs
@@ -54,21 +54,21 @@ Config: `name`, `description?`, `inputSchema`, `outputSchema?`, `budget?` (`maxC
54
54
 
55
55
  **The call site places.** Every builder call — `then`, `agentStep`, `specialistStep`, `toolStep`, `map`, `approval`, `waitForSignal`, `sleep`, `sleepUntil`, `foreach`, `parallel`, `branch`, `switch`, `dowhile`, `dountil`, `workflow` — appends **exactly one** entry to the chain where it is called. Containers take `StepRef`s: an inline step object, or a **string** naming an `agentStep`/`specialistStep`/`toolStep` declared elsewhere in the same chain — a string ref inside a container means "declare here, inside me" and does not append a second top-level entry. An id declared but never placed is `WORKFLOW_UNPLACED_STEP` (warning locally, error at push); referenced but never declared is `unknown-step-ref` at `.commit()`.
56
56
 
57
- | Verb | Entry |
58
- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
- | `.then(step, input?)` | Sequential step |
60
- | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })` | Agent turn (`prompt` is a `template(...)`) |
61
- | `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
62
- | `.toolStep(id, { toolId, input? })` | One tool call — `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
- | `.map(descriptors, { id })` | Data reshaping — `id` is **required** once the workflow has ≥ 2 maps (`map-id-required`) |
64
- | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
65
- | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
- | `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })` | Fan-out over an upstream array. `items` lowers to a `<bodyId>_items` mapping ahead of the entry and takes `fromInit(path)` / `fromStep(id, path)` or an `init(...)` / `step(...).path(...)` ref (LUA-864) — the node is byte-identical whichever form you write; any other descriptor (`value`, `template`, `fromRequest`, `rows`, `fromKnowledge`, a fan-in `fromStep([...])`) is `invalid-envelope` at build: `.map({ '': ... }, { id })` before the foreach instead |
67
- | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
- | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits — `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
- | `.approval(id, { title, approver, timeoutHours, onTimeout, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
70
- | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` — completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, …) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) — see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
71
- | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
57
+ | Verb | Entry |
58
+ | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `.then(step, input?)` | Sequential step |
60
+ | `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })` | Agent turn (`prompt` is a `template(...)`) |
61
+ | `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
62
+ | `.toolStep(id, { toolId, input? })` | One tool call — `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
63
+ | `.map(descriptors, { id })` | Data reshaping — `id` is **required** once the workflow has ≥ 2 maps (`map-id-required`) |
64
+ | `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
65
+ | `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
66
+ | `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })` | Fan-out over an upstream array. `items` lowers to a `<bodyId>_items` mapping ahead of the entry and takes `fromInit(path)` / `fromStep(id, path)` or an `init(...)` / `step(...).path(...)` ref (LUA-864) — the node is byte-identical whichever form you write; any other descriptor (`value`, `template`, `fromRequest`, `rows`, `fromKnowledge`, a fan-in `fromStep([...])`) is `invalid-envelope` at build: `.map({ '': ... }, { id })` before the foreach instead |
67
+ | `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
68
+ | `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits — `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
69
+ | `.approval(id, { title, approver, timeoutHours, onTimeout, remindAfterHours?, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
70
+ | `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` — completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, …) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) — see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
71
+ | `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
72
72
 
73
73
  ### Typed predicates
74
74
 
@@ -30,14 +30,16 @@ The Orders API enables:
30
30
  ```typescript
31
31
  enum OrderStatus {
32
32
  PENDING = 'pending',
33
- PROCESSING = 'processing',
34
- SHIPPED = 'shipped',
35
- DELIVERED = 'delivered',
36
- CANCELLED = 'cancelled',
37
- REFUNDED = 'refunded'
33
+ CONFIRMED = 'confirmed',
34
+ FULFILLED = 'fulfilled',
35
+ CANCELLED = 'cancelled'
38
36
  }
39
37
  ```
40
38
 
39
+ > **Note:** the platform spells the fulfilled status `fullfilled`. `OrderStatus.FULFILLED` is sent to the platform as `fullfilled` (for both `updateStatus` and `get(status)`), and `order.common.status` reads back as `fullfilled`. Any other status value is rejected with HTTP 400, and an unknown order id answers HTTP 404; both are thrown as an `Error` carrying the server message.
40
+ >
41
+ > `Orders.getById`, `updateStatus` and `updateData` take the order `id` (the 24-character hex returned by `create`/`get`), not the `orderId` display string; an id that is not 24-hex answers HTTP 404. An unexpected platform failure on `Orders.create` answers HTTP 424 and a failure on the other calls answers HTTP 500; both are thrown as an `Error` with the generic message `Order request failed`.
42
+
41
43
  ## OrderInstance Methods
42
44
 
43
45
  Once you have an order instance, you can call these methods:
@@ -101,7 +101,7 @@ Searches products by name or description.
101
101
 
102
102
  **Parameters:**
103
103
  - `query` (string) - Search term
104
- - `limit` (number, optional) - Max results (default: 10)
104
+ - `limit` (number, optional) - Max results (default: 5)
105
105
 
106
106
  **Returns:** `Promise<ProductSearchInstance>`
107
107
 
@@ -0,0 +1,57 @@
1
+ # Progress API
2
+
3
+ Show live progress for a long-running tool in the chat that called it.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { Progress } from 'lua-cli';
9
+ ```
10
+
11
+ ## `Progress.update(input?)`
12
+
13
+ Shows or updates ONE progress card for the current tool call. The card closes on its own when the tool returns
14
+ (as done, or as failed when the tool throws).
15
+
16
+ **Parameters** (all optional):
17
+
18
+ - `label` (string, up to 120 characters) — the card title. When you leave it out, the previous label is kept; the
19
+ first call without one uses the tool's name.
20
+ - `fraction` (number) — progress from 0 to 1 (clamped). Leave it out for an indeterminate bar.
21
+ - `phase` (string, up to 200 characters) — what the tool is doing now, e.g. `"Reading sources (3/12)"`.
22
+
23
+ **Returns:** `Promise<{ emitted: boolean }>`
24
+
25
+ - `emitted: true` — the card was shown. Calls closer together than 250 ms are coalesced: the latest one is shown.
26
+ - `emitted: false` — there is nothing to show it on: the chat surface cannot render progress cards (for example
27
+ WhatsApp, or an API call without a live stream), the tool is not running in a chat turn (jobs, webhooks,
28
+ triggers), the feature is not enabled for your environment, or you are running offline in `lua test`.
29
+
30
+ `Progress.update` never throws. You do not need to wrap it in `try`/`catch`.
31
+
32
+ ## Example
33
+
34
+ ```typescript
35
+ import { Progress } from 'lua-cli';
36
+ import { LuaTool } from 'lua-cli/skill';
37
+ import { z } from 'zod';
38
+
39
+ export default class ImportFilesTool implements LuaTool {
40
+ name = 'import_files';
41
+ description = 'Import the given files';
42
+ inputSchema = z.object({ urls: z.array(z.string()) });
43
+
44
+ async execute({ urls }: z.infer<typeof this.inputSchema>) {
45
+ for (let i = 0; i < urls.length; i++) {
46
+ await Progress.update({ label: 'Importing files', fraction: i / urls.length, phase: urls[i] });
47
+ await importOne(urls[i]);
48
+ }
49
+ return { imported: urls.length };
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Workflow runs started from a tool
55
+
56
+ When a tool starts a workflow run with `Workflows.start(...)` during a chat turn, the run's live card appears in
57
+ that chat (the run's origin thread), the same card a run started by the model shows. No code is needed.