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

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/docs/api/Data.md CHANGED
@@ -11,6 +11,7 @@ import { Data } from 'lua-cli';
11
11
  ## Overview
12
12
 
13
13
  The Data API is perfect for:
14
+
14
15
  - Custom data collections (movies, recipes, documents)
15
16
  - Vector/semantic search
16
17
  - User-specific data storage
@@ -24,6 +25,7 @@ The Data API is perfect for:
24
25
  Retrieves entries from a collection with optional filtering and pagination.
25
26
 
26
27
  **Parameters:**
28
+
27
29
  - `collectionName` (string) - Name of the collection
28
30
  - `filter` (object, optional) - MongoDB-style filter criteria
29
31
  - `page` (number, optional) - Page number (default: 1)
@@ -48,6 +50,7 @@ Retrieves entries from a collection with optional filtering and pagination.
48
50
  ```
49
51
 
50
52
  **Example:**
53
+
51
54
  ```typescript
52
55
  // Get all (first page)
53
56
  const result = await Data.get('movies');
@@ -69,6 +72,7 @@ const sciFi = await Data.get('movies', { genre: 'Sci-Fi' });
69
72
  Performs semantic search on a collection using AI embeddings.
70
73
 
71
74
  **Parameters:**
75
+
72
76
  - `collectionName` (string) - Collection to search
73
77
  - `query` (string) - Search query (uses AI embeddings)
74
78
  - `limit` (number, optional) - Max results (default: 10)
@@ -79,6 +83,7 @@ Performs semantic search on a collection using AI embeddings.
79
83
  Each result includes a `score` property indicating similarity (0-1).
80
84
 
81
85
  **Example:**
86
+
82
87
  ```typescript
83
88
  const results = await Data.search('movies', 'sci-fi action thriller', 5, 0.7);
84
89
 
@@ -93,6 +98,7 @@ for (const movie of results) {
93
98
  Creates a new entry in a collection.
94
99
 
95
100
  **Parameters:**
101
+
96
102
  - `collectionName` (string) - Collection name
97
103
  - `data` (object) - Data to store
98
104
  - `searchText` (string, optional) - Text to use for vector search indexing
@@ -100,13 +106,18 @@ Creates a new entry in a collection.
100
106
  **Returns:** `Promise<DataEntryInstance>`
101
107
 
102
108
  **Example:**
109
+
103
110
  ```typescript
104
- const movie = await Data.create('movies', {
105
- title: 'Inception',
106
- year: 2010,
107
- director: 'Christopher Nolan',
108
- genre: ['Sci-Fi', 'Thriller']
109
- }, 'Inception 2010 sci-fi thriller Christopher Nolan dream heist');
111
+ const movie = await Data.create(
112
+ 'movies',
113
+ {
114
+ title: 'Inception',
115
+ year: 2010,
116
+ director: 'Christopher Nolan',
117
+ genre: ['Sci-Fi', 'Thriller'],
118
+ },
119
+ 'Inception 2010 sci-fi thriller Christopher Nolan dream heist'
120
+ );
110
121
 
111
122
  console.log('Created:', movie.id);
112
123
  ```
@@ -116,6 +127,7 @@ console.log('Created:', movie.id);
116
127
  Updates an existing entry.
117
128
 
118
129
  **Parameters:**
130
+
119
131
  - `collectionName` (string) - Collection name
120
132
  - `entryId` (string) - Entry ID to update
121
133
  - `data` (object) - Updated data
@@ -124,30 +136,140 @@ Updates an existing entry.
124
136
  **Returns:** `Promise<UpdateCustomDataResponse>`
125
137
 
126
138
  **Example:**
139
+
127
140
  ```typescript
128
- const result = await Data.update(
129
- 'movies',
130
- 'entry_123',
131
- { rating: 9.5, views: 1000 },
132
- 'Inception highly rated popular'
133
- );
141
+ const result = await Data.update('movies', 'entry_123', { rating: 9.5, views: 1000 }, 'Inception highly rated popular');
134
142
 
135
143
  if (result.status === 'success') {
136
144
  console.log(result.message); // "Custom data entry updated"
137
145
  }
138
146
  ```
139
147
 
148
+ ### `Data.patch(collectionName, entryId, mutation, options?)`
149
+
150
+ Atomically sets and removes top-level fields in **one** write. Unlike `Data.update` (which reads the entry,
151
+ merges your fields and writes the whole `data` back, so concurrent updates can overwrite each other), `patch`
152
+ only touches the fields you name.
153
+
154
+ **Parameters:**
155
+
156
+ - `collectionName` (string) - Collection name
157
+ - `entryId` (string) - Entry ID
158
+ - `mutation` (object) - `{ set?: Record<string, any>; unset?: string[]; searchText?: string | null }`
159
+ (`searchText: null` clears the entry's search text and vector)
160
+ - `options` (object, optional) - `{ ifVersion?: number }` for compare-and-swap (below)
161
+
162
+ **Returns:** `Promise<UpdateCustomDataResponse>` - `{ status, message, version }`, where `version` is the entry's
163
+ new version
164
+
165
+ ```typescript
166
+ await Data.patch('tickets', ticketId, { set: { status: 'closed' }, unset: ['assignee'] });
167
+ ```
168
+
169
+ #### Compare-and-swap with `ifVersion`
170
+
171
+ Every entry carries a `version` number that **every** write (`create`, `update`, `patch`, `increment`)
172
+ bumps. Read it as `entry.version` on the entries `Data.get` returns, or `entry._version` on the
173
+ `DataEntryInstance` that `Data.getEntry` / `Data.create` return (underscored so it never hides a `version`
174
+ field of your own data). Entries not written since versioning was introduced report `0`.
175
+
176
+ Pass the version you read as `ifVersion` and the patch applies **only if nobody wrote the entry in between**.
177
+ Otherwise nothing is written and the call rejects with an error whose `code` is `'data_version_conflict'`
178
+ (`status` 409); `error.currentVersion` carries the version to re-read from and `error.expectedVersion` the
179
+ stale one you sent. This is the same in `lua test`, in deployed agents and in voice agents.
180
+
181
+ `ifVersion` must be a non-negative integer. If the `ifVersion` key is present with any other value,
182
+ including `undefined`, the call throws a `TypeError` and sends nothing. It is never quietly dropped, which
183
+ would turn the compare-and-swap into an unconditional write. Leave the key out entirely for an unconditional
184
+ patch, so build options with a conditional spread: `{ ...(v !== undefined ? { ifVersion: v } : {}) }`.
185
+
186
+ ```typescript
187
+ // Claim a seat exactly once, even if two conversations race for it.
188
+ async function claimSeat(seatId: string, userId: string): Promise<boolean> {
189
+ for (let attempt = 0; attempt < 3; attempt++) {
190
+ const seat = await Data.getEntry('seats', seatId);
191
+ if (seat.data.heldBy) return false;
192
+ try {
193
+ await Data.patch('seats', seatId, { set: { heldBy: userId } }, { ifVersion: seat._version });
194
+ return true;
195
+ } catch (error: any) {
196
+ if (error?.code !== 'data_version_conflict') throw error;
197
+ // Someone else wrote the seat first: loop and re-read.
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+ ```
203
+
204
+ Compare `error.code` against the string `'data_version_conflict'` rather than using `instanceof`.
205
+
206
+ **Notes:**
207
+
208
+ - A patch with `ifVersion` is sent once and is not retried, because a retry of an attempt that landed would
209
+ find its own version bump and report a false conflict. So a network error leaves the outcome unknown,
210
+ just as with `increment`. After a network error, re-read the entry: if your change is there, your write
211
+ landed.
212
+ - Versions start at `1` when an entry is created, including by an `increment` upsert. If an entry is deleted
213
+ and an entry with the **same ID** is created again, its version restarts, and an `ifVersion` read from the
214
+ deleted entry can match the new one. Do not reuse the IDs of deleted entries for state you guard with
215
+ `ifVersion`.
216
+
217
+ ### `Data.increment(collectionName, entryId, field, by?, options?)`
218
+
219
+ Atomically adds `by` to a top-level numeric field and resolves the field's **new value**. Simultaneous
220
+ increments never lose a count (each is a single database operation), which a `get` + `update` cannot
221
+ guarantee.
222
+
223
+ **Parameters:**
224
+
225
+ - `collectionName` (string) - Collection name
226
+ - `entryId` (string) - Entry ID (the ID to create under when upserting)
227
+ - `field` (string) - Top-level field name (no dots, no leading `$`)
228
+ - `by` (number, optional) - Amount to add; default `1`. Negative and fractional amounts are allowed
229
+ - `options` (object, optional) - `{ upsert?: boolean }`. With `upsert: true` a missing entry is created as
230
+ `{ [field]: by }` instead of failing
231
+
232
+ **Returns:** `Promise<number>` - the field's value after the increment
233
+
234
+ **Errors:** 404 when the entry does not exist and `upsert` is not set; 400 when the field (or the entry's
235
+ `data`) is not numeric. A missing field starts at `0`.
236
+
237
+ ```typescript
238
+ const views = await Data.increment('stats', 'homepage', 'views'); // 1, 2, 3, ...
239
+ await Data.increment('inventory', sku, 'available', -1); // decrement
240
+
241
+ // A daily counter: one long-lived entry, one FIELD per day. The entry is created once (see the note
242
+ // below); each new day's field simply starts at 0, so no conversation ever has to create anything.
243
+ await Data.increment('stats', 'calls', `day-${today}`); // `today` like '2026-10-01'
244
+ ```
245
+
246
+ **Notes:**
247
+
248
+ - Upsert race: `upsert` is **not** safe for an entry that many conversations may create at the same moment.
249
+ Two first-time upserts of the same new ID that run concurrently can each create an entry. Later
250
+ increments then land on only one of them, and the count in the other is lost. So do not upsert a fresh
251
+ ID per day or per event under load (for example
252
+ ``Data.increment('daily', `calls-${today}`, 'count', 1, { upsert: true })``). Create the entry once
253
+ from a path that does not run concurrently, such as a setup job or
254
+ `Data.increment('stats', 'calls', 'total', 0, { upsert: true })` before go-live. After that,
255
+ increment it, or add new fields to it, as above.
256
+ - The SDK sends the request once and does not retry it: an increment is not idempotent, so after a network
257
+ error the count may or may not have been applied. Do not blindly retry an increment that failed with a
258
+ network error if an exact count matters.
259
+
140
260
  ### `Data.delete(collectionName, entryId)`
141
261
 
142
262
  Deletes an entry from a collection.
143
263
 
144
264
  **Parameters:**
265
+
145
266
  - `collectionName` (string) - Collection name
146
267
  - `entryId` (string) - Entry ID to delete
147
268
 
148
269
  **Returns:** `Promise<DeleteCustomDataResponse>`
149
270
 
150
271
  **Example:**
272
+
151
273
  ```typescript
152
274
  const result = await Data.delete('movies', 'entry_123');
153
275
 
@@ -165,13 +287,14 @@ Data entries have convenient methods:
165
287
  Updates this entry.
166
288
 
167
289
  **Example:**
290
+
168
291
  ```typescript
169
292
  const movies = await Data.get('movies');
170
293
  const movie = movies[0];
171
294
 
172
295
  await movie.update({
173
296
  rating: 9.0,
174
- views: movie.views + 1
297
+ views: movie.views + 1,
175
298
  });
176
299
  ```
177
300
 
@@ -180,9 +303,10 @@ await movie.update({
180
303
  Deletes this entry.
181
304
 
182
305
  **Example:**
306
+
183
307
  ```typescript
184
308
  const movies = await Data.get('movies');
185
- const oldMovie = movies.find(m => m.year < 1950);
309
+ const oldMovie = movies.find((m) => m.year < 1950);
186
310
 
187
311
  if (oldMovie) {
188
312
  await oldMovie.delete();
@@ -194,6 +318,7 @@ if (oldMovie) {
194
318
  Saves modifications to this entry.
195
319
 
196
320
  **Example:**
321
+
197
322
  ```typescript
198
323
  const movies = await Data.get('movies');
199
324
  const movie = movies[0];
@@ -236,7 +361,7 @@ import { z } from 'zod';
236
361
  export default class MovieDatabaseTool implements LuaTool {
237
362
  name = 'manage_movies';
238
363
  description = 'Manage a movie database';
239
-
364
+
240
365
  inputSchema = z.object({
241
366
  action: z.enum(['add', 'search', 'update', 'delete']),
242
367
  title: z.string().optional(),
@@ -245,78 +370,82 @@ export default class MovieDatabaseTool implements LuaTool {
245
370
  genre: z.array(z.string()).optional(),
246
371
  rating: z.number().min(0).max(10).optional(),
247
372
  movieId: z.string().optional(),
248
- query: z.string().optional()
373
+ query: z.string().optional(),
249
374
  });
250
-
375
+
251
376
  async execute(input: z.infer<typeof this.inputSchema>) {
252
377
  switch (input.action) {
253
378
  case 'add':
254
379
  if (!input.title || !input.year) {
255
380
  return { success: false, error: 'Title and year required' };
256
381
  }
257
-
382
+
258
383
  const searchText = `${input.title} ${input.year} ${input.director || ''} ${input.genre?.join(' ') || ''}`;
259
-
260
- const movie = await Data.create('movies', {
261
- title: input.title,
262
- year: input.year,
263
- director: input.director,
264
- genre: input.genre || [],
265
- rating: input.rating || 0,
266
- createdAt: new Date().toISOString()
267
- }, searchText);
268
-
384
+
385
+ const movie = await Data.create(
386
+ 'movies',
387
+ {
388
+ title: input.title,
389
+ year: input.year,
390
+ director: input.director,
391
+ genre: input.genre || [],
392
+ rating: input.rating || 0,
393
+ createdAt: new Date().toISOString(),
394
+ },
395
+ searchText
396
+ );
397
+
269
398
  return {
270
399
  success: true,
271
400
  message: `Added "${input.title}"`,
272
- movieId: movie.id
401
+ movieId: movie.id,
273
402
  };
274
-
403
+
275
404
  case 'search':
276
405
  if (!input.query) {
277
406
  return { success: false, error: 'Query required' };
278
407
  }
279
-
408
+
280
409
  const results = await Data.search('movies', input.query, 10);
281
-
410
+
282
411
  return {
283
412
  success: true,
284
413
  count: results.data.length,
285
- movies: results.data.map(r => ({
414
+ movies: results.data.map((r) => ({
286
415
  id: r.id,
287
416
  title: r.data.title,
288
417
  year: r.data.year,
289
418
  director: r.data.director,
290
- relevance: r.score
291
- }))
419
+ relevance: r.score,
420
+ })),
292
421
  };
293
-
422
+
294
423
  case 'update':
295
424
  if (!input.movieId) {
296
425
  return { success: false, error: 'Movie ID required' };
297
426
  }
298
-
427
+
299
428
  const updates: any = {};
300
429
  if (input.rating !== undefined) updates.rating = input.rating;
301
430
  if (input.director) updates.director = input.director;
302
-
431
+
303
432
  const updateResult = await Data.update('movies', input.movieId, updates);
304
-
433
+
305
434
  return {
306
435
  success: updateResult.status === 'success',
307
- message: updateResult.message
436
+ message: updateResult.message,
308
437
  };
309
-
438
+
310
439
  case 'delete':
311
440
  if (!input.movieId) {
312
441
  return { success: false, error: 'Movie ID required' };
313
442
  }
314
-
443
+
315
444
  const deleteResult = await Data.delete('movies', input.movieId);
316
-
445
+
317
446
  return {
318
447
  success: deleteResult.status === 'success',
319
- message: deleteResult.message
448
+ message: deleteResult.message,
320
449
  };
321
450
  }
322
451
  }
@@ -332,33 +461,33 @@ import { z } from 'zod';
332
461
  export default class KnowledgeBaseTool implements LuaTool {
333
462
  name = 'search_knowledge_base';
334
463
  description = 'Search company knowledge base for information';
335
-
464
+
336
465
  inputSchema = z.object({
337
- question: z.string().describe('User question')
466
+ question: z.string().describe('User question'),
338
467
  });
339
-
468
+
340
469
  async execute(input: z.infer<typeof this.inputSchema>) {
341
470
  // Semantic search finds relevant articles
342
471
  const results = await Data.search('knowledge-base', input.question, 3);
343
-
472
+
344
473
  if (results.data.length === 0) {
345
474
  return {
346
475
  success: true,
347
476
  found: false,
348
- message: 'No relevant articles found'
477
+ message: 'No relevant articles found',
349
478
  };
350
479
  }
351
-
480
+
352
481
  // Return top results
353
482
  return {
354
483
  success: true,
355
484
  found: true,
356
- articles: results.data.map(r => ({
485
+ articles: results.data.map((r) => ({
357
486
  title: r.data.title,
358
487
  content: r.data.content,
359
488
  relevance: Math.round(r.score * 100),
360
- url: r.data.url
361
- }))
489
+ url: r.data.url,
490
+ })),
362
491
  };
363
492
  }
364
493
  }
@@ -373,27 +502,27 @@ import { z } from 'zod';
373
502
  export default class RecipeTool implements LuaTool {
374
503
  name = 'find_recipe';
375
504
  description = 'Find recipes based on ingredients or cuisine';
376
-
505
+
377
506
  inputSchema = z.object({
378
- searchTerm: z.string().describe('Ingredients, cuisine, or dish name')
507
+ searchTerm: z.string().describe('Ingredients, cuisine, or dish name'),
379
508
  });
380
-
509
+
381
510
  async execute(input: z.infer<typeof this.inputSchema>) {
382
511
  // Search recipes
383
512
  const results = await Data.search('recipes', input.searchTerm, 5);
384
-
513
+
385
514
  return {
386
515
  success: true,
387
516
  count: results.data.length,
388
- recipes: results.data.map(r => ({
517
+ recipes: results.data.map((r) => ({
389
518
  name: r.data.name,
390
519
  cuisine: r.data.cuisine,
391
520
  ingredients: r.data.ingredients,
392
521
  instructions: r.data.instructions,
393
522
  prepTime: r.data.prepTime,
394
523
  difficulty: r.data.difficulty,
395
- matchScore: Math.round(r.score * 100)
396
- }))
524
+ matchScore: Math.round(r.score * 100),
525
+ })),
397
526
  };
398
527
  }
399
528
  }
@@ -413,6 +542,7 @@ await Data.search('movies', 'Star Wars type movies');
413
542
  ```
414
543
 
415
544
  **Tips:**
545
+
416
546
  - Use descriptive search text when creating entries
417
547
  - Search queries can be natural language
418
548
  - Results include similarity scores (0-1)
@@ -421,6 +551,7 @@ await Data.search('movies', 'Star Wars type movies');
421
551
  ## Best Practices
422
552
 
423
553
  ### ✅ DO
554
+
424
555
  - Use descriptive collection names (`user-notes`, not `notes`)
425
556
  - Provide searchText for better semantic search
426
557
  - Include all relevant keywords in searchText
@@ -428,6 +559,7 @@ await Data.search('movies', 'Star Wars type movies');
428
559
  - Use meaningful property names
429
560
 
430
561
  ### ❌ DON'T
562
+
431
563
  - Don't store large binary data (use URLs instead)
432
564
  - Don't use Data API for relational data (use Products/Orders)
433
565
  - Don't skip searchText if you plan to search
@@ -443,4 +575,3 @@ await Data.search('movies', 'Star Wars type movies');
443
575
 
444
576
  - [Custom Data Tool Example](../../template/src/skills/tools/CustomDataTool.ts)
445
577
  - [Vector Search Guide](../guides/VectorSearch.md)
446
-
package/docs/api/Jobs.md CHANGED
@@ -30,6 +30,8 @@ Creates a new scheduled job.
30
30
 
31
31
  **Returns:** `Promise<JobInstance>`
32
32
 
33
+ Dynamic jobs are stored as `<name> - <epoch ms>`, the same under `lua test` and deployed.
34
+
33
35
  **Example:**
34
36
  ```typescript
35
37
  const job = await Jobs.create({