lua-cli 2.5.8 → 3.0.0-alpha.4
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/dist/api/job.api.service.d.ts +219 -0
- package/dist/api/job.api.service.js +216 -0
- package/dist/api/lazy-instances.d.ts +24 -0
- package/dist/api/lazy-instances.js +48 -0
- package/dist/api/postprocessor.api.service.d.ts +158 -0
- package/dist/api/postprocessor.api.service.js +111 -0
- package/dist/api/preprocessor.api.service.d.ts +158 -0
- package/dist/api/preprocessor.api.service.js +111 -0
- package/dist/api/user.data.api.service.d.ts +13 -0
- package/dist/api/user.data.api.service.js +20 -0
- package/dist/api/webhook.api.service.d.ts +151 -0
- package/dist/api/webhook.api.service.js +134 -0
- package/dist/api-exports.d.ts +176 -41
- package/dist/api-exports.js +195 -21
- package/dist/cli/command-definitions.js +75 -5
- package/dist/commands/chat.js +32 -5
- package/dist/commands/compile.js +140 -7
- package/dist/commands/dev.js +23 -2
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +53 -7
- package/dist/commands/jobs.d.ts +20 -0
- package/dist/commands/jobs.js +533 -0
- package/dist/commands/logs.js +2 -5
- package/dist/commands/postprocessors.d.ts +8 -0
- package/dist/commands/postprocessors.js +431 -0
- package/dist/commands/preprocessors.d.ts +8 -0
- package/dist/commands/preprocessors.js +431 -0
- package/dist/commands/push.js +686 -5
- package/dist/commands/test.d.ts +9 -18
- package/dist/commands/test.js +574 -82
- package/dist/commands/webhooks.d.ts +18 -0
- package/dist/commands/webhooks.js +424 -0
- package/dist/common/job.instance.d.ts +80 -0
- package/dist/common/job.instance.js +116 -0
- package/dist/common/user.instance.d.ts +1 -0
- package/dist/common/user.instance.js +9 -0
- package/dist/config/constants.d.ts +4 -3
- package/dist/config/constants.js +10 -8
- package/dist/interfaces/agent.d.ts +2 -1
- package/dist/interfaces/chat.d.ts +52 -1
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.js +7 -0
- package/dist/interfaces/jobs.d.ts +193 -0
- package/dist/interfaces/jobs.js +5 -0
- package/dist/interfaces/postprocessors.d.ts +35 -0
- package/dist/interfaces/postprocessors.js +4 -0
- package/dist/interfaces/preprocessors.d.ts +35 -0
- package/dist/interfaces/preprocessors.js +4 -0
- package/dist/interfaces/webhooks.d.ts +104 -0
- package/dist/interfaces/webhooks.js +5 -0
- package/dist/types/api-contracts.d.ts +5 -0
- package/dist/types/compile.types.d.ts +49 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/skill.d.ts +521 -0
- package/dist/types/skill.js +471 -0
- package/dist/utils/agent-management.d.ts +25 -0
- package/dist/utils/agent-management.js +67 -0
- package/dist/utils/bundling.d.ts +44 -5
- package/dist/utils/bundling.js +723 -23
- package/dist/utils/compile.d.ts +63 -0
- package/dist/utils/compile.js +712 -36
- package/dist/utils/deployment.d.ts +2 -1
- package/dist/utils/deployment.js +16 -2
- package/dist/utils/dev-api.d.ts +42 -2
- package/dist/utils/dev-api.js +177 -4
- package/dist/utils/dev-server.d.ts +1 -1
- package/dist/utils/dev-server.js +4 -4
- package/dist/utils/dynamic-job-bundler.d.ts +17 -0
- package/dist/utils/dynamic-job-bundler.js +143 -0
- package/dist/utils/init-agent.d.ts +3 -1
- package/dist/utils/init-agent.js +6 -4
- package/dist/utils/init-prompts.d.ts +2 -1
- package/dist/utils/init-prompts.js +14 -9
- package/dist/utils/job-management.d.ts +24 -0
- package/dist/utils/job-management.js +264 -0
- package/dist/utils/postprocessor-management.d.ts +9 -0
- package/dist/utils/postprocessor-management.js +118 -0
- package/dist/utils/pre-bundle-jobs.d.ts +26 -0
- package/dist/utils/pre-bundle-jobs.js +176 -0
- package/dist/utils/preprocessor-management.d.ts +9 -0
- package/dist/utils/preprocessor-management.js +118 -0
- package/dist/utils/sandbox-storage.d.ts +48 -0
- package/dist/utils/sandbox-storage.js +114 -0
- package/dist/utils/sandbox.d.ts +61 -1
- package/dist/utils/sandbox.js +299 -72
- package/dist/utils/tool-detection.d.ts +3 -2
- package/dist/utils/tool-detection.js +18 -4
- package/dist/utils/webhook-management.d.ts +24 -0
- package/dist/utils/webhook-management.js +256 -0
- package/package.json +1 -1
- package/template/README.md +30 -2
- package/template/lua.skill.yaml +47 -0
- package/template/package-lock.json +10505 -0
- package/template/package.json +2 -1
- package/template/src/index.ts +103 -2
- package/template/src/jobs/AbandonedBasketProcessorJob.ts +139 -0
- package/template/src/jobs/DailyCleanupJob.ts +100 -0
- package/template/src/jobs/DataMigrationJob.ts +133 -0
- package/template/src/jobs/HealthCheckJob.ts +87 -0
- package/template/src/tools/CreateInlineJob.ts +42 -0
- package/template/src/tools/GameScoreTrackerTool.ts +356 -0
- package/template/src/tools/SmartBasketTool.ts +188 -0
- package/template/src/webhooks/PaymentWebhook.ts +113 -0
- package/template/src/webhooks/UserEventWebhook.ts +77 -0
- package/API_REFERENCE.md +0 -1408
- package/CHANGELOG.md +0 -236
- package/CLI_REFERENCE.md +0 -908
- package/GETTING_STARTED.md +0 -1040
- package/INSTANCE_TYPES.md +0 -1158
- package/README.md +0 -865
- package/TEMPLATE_GUIDE.md +0 -1398
- package/USER_DATA_INSTANCE.md +0 -621
- package/template/TOOL_EXAMPLES.md +0 -655
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Game Score Tracker Tool - Complex Dynamic Job Example
|
|
3
|
+
*
|
|
4
|
+
* This tool demonstrates:
|
|
5
|
+
* 1. Creating a game tracking record
|
|
6
|
+
* 2. Creating an interval job that starts at game time
|
|
7
|
+
* 3. Job fetches score updates periodically
|
|
8
|
+
* 4. Job checks if game is over
|
|
9
|
+
* 5. Job deactivates itself when game ends
|
|
10
|
+
*
|
|
11
|
+
* Use case: Live sports score tracking, esports matches, auction monitoring
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { LuaTool, Jobs, Data } from "lua-cli";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
export class TrackGameScoresTool implements LuaTool {
|
|
18
|
+
name = "track_game_scores";
|
|
19
|
+
description = "Track live scores for a game with automatic updates";
|
|
20
|
+
|
|
21
|
+
inputSchema = z.object({
|
|
22
|
+
gameId: z.string(),
|
|
23
|
+
gameName: z.string(),
|
|
24
|
+
startTime: z.string(), // ISO 8601 timestamp
|
|
25
|
+
updateIntervalSeconds: z.number().min(30).max(600).default(60), // 30s to 10min
|
|
26
|
+
apiEndpoint: z.string().optional() // Optional external API
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async execute(input: {
|
|
30
|
+
gameId: string;
|
|
31
|
+
gameName: string;
|
|
32
|
+
startTime: string;
|
|
33
|
+
updateIntervalSeconds: number;
|
|
34
|
+
apiEndpoint?: string;
|
|
35
|
+
}) {
|
|
36
|
+
console.log(`🎮 Setting up score tracking for: ${input.gameName}`);
|
|
37
|
+
|
|
38
|
+
// Step 1: Create game record in database
|
|
39
|
+
const gameRecord = {
|
|
40
|
+
gameId: input.gameId,
|
|
41
|
+
gameName: input.gameName,
|
|
42
|
+
startTime: input.startTime,
|
|
43
|
+
status: 'scheduled',
|
|
44
|
+
currentScore: { home: 0, away: 0 },
|
|
45
|
+
lastUpdate: new Date().toISOString(),
|
|
46
|
+
createdAt: new Date().toISOString()
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const game = await Data.create('games', gameRecord,
|
|
50
|
+
`${input.gameName} ${input.gameId}`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
console.log(`✅ Game record created: ${game.id}`);
|
|
54
|
+
|
|
55
|
+
// Step 2: Calculate when the job should start
|
|
56
|
+
const gameStartTime = new Date(input.startTime);
|
|
57
|
+
const now = new Date();
|
|
58
|
+
|
|
59
|
+
// If game hasn't started yet, schedule job to start at game time
|
|
60
|
+
// If game already started, start immediately
|
|
61
|
+
const jobStartTime = gameStartTime > now ? gameStartTime : now;
|
|
62
|
+
|
|
63
|
+
console.log(`⏰ Job will start at: ${jobStartTime.toLocaleString()}`);
|
|
64
|
+
console.log(`🔄 Updates every ${input.updateIntervalSeconds} seconds`);
|
|
65
|
+
|
|
66
|
+
// Step 3: Capture variables for the job closure
|
|
67
|
+
const gameId = input.gameId;
|
|
68
|
+
const gameName = input.gameName;
|
|
69
|
+
const intervalSeconds = input.updateIntervalSeconds;
|
|
70
|
+
const apiEndpoint = input.apiEndpoint || 'https://api.example.com/scores';
|
|
71
|
+
const gameRecordId = game.id;
|
|
72
|
+
|
|
73
|
+
// Step 4: Create an interval job that monitors the game
|
|
74
|
+
// This job will run every X seconds and stop itself when game ends
|
|
75
|
+
const job = await Jobs.create({
|
|
76
|
+
name: `track-game-${gameId}`,
|
|
77
|
+
description: `Live score tracking for ${gameName}`,
|
|
78
|
+
|
|
79
|
+
// Important: Use interval schedule
|
|
80
|
+
schedule: {
|
|
81
|
+
type: 'interval',
|
|
82
|
+
seconds: intervalSeconds
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
timeout: 30, // Each update should complete in 30 seconds
|
|
86
|
+
|
|
87
|
+
retry: {
|
|
88
|
+
maxAttempts: 2, // Retry failed updates
|
|
89
|
+
backoffSeconds: 10
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// The job logic - runs every interval until game ends
|
|
93
|
+
execute: async () => {
|
|
94
|
+
console.log('🎮 Fetching score update for game ' + gameId + '...');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Fetch current game data from our database
|
|
98
|
+
const gameData = await Data.getEntry('games', gameRecordId);
|
|
99
|
+
|
|
100
|
+
// Check if game is already over
|
|
101
|
+
if (gameData.data.status === 'finished') {
|
|
102
|
+
console.log('🏁 Game ' + gameId + ' is already finished. Stopping job.');
|
|
103
|
+
|
|
104
|
+
// IMPORTANT: Job stops itself!
|
|
105
|
+
await Jobs.deactivate(job.jobId);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
action: 'job-stopped',
|
|
109
|
+
reason: 'game-finished',
|
|
110
|
+
gameId: gameId,
|
|
111
|
+
finalScore: gameData.data.currentScore
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fetch latest scores from external API
|
|
116
|
+
let scoreUpdate;
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(apiEndpoint + '/' + gameId);
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error('API returned ' + response.status);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scoreUpdate = await response.json();
|
|
125
|
+
} catch (apiError) {
|
|
126
|
+
// If external API fails, generate mock data for demo
|
|
127
|
+
console.warn('External API failed, using mock data:', apiError);
|
|
128
|
+
|
|
129
|
+
// Simulate score progression
|
|
130
|
+
const currentScore = gameData.data.currentScore;
|
|
131
|
+
scoreUpdate = {
|
|
132
|
+
home: currentScore.home + Math.floor(Math.random() * 2),
|
|
133
|
+
away: currentScore.away + Math.floor(Math.random() * 2),
|
|
134
|
+
quarter: Math.min(4, Math.floor(Date.now() / 1000 / 60 / 15) % 5),
|
|
135
|
+
timeRemaining: '12:34',
|
|
136
|
+
isFinished: Math.random() > 0.9 // 10% chance game ends
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Update game record with new scores
|
|
141
|
+
const updatedGame = await Data.update('games', gameRecordId, {
|
|
142
|
+
...gameData.data,
|
|
143
|
+
currentScore: {
|
|
144
|
+
home: scoreUpdate.home,
|
|
145
|
+
away: scoreUpdate.away
|
|
146
|
+
},
|
|
147
|
+
quarter: scoreUpdate.quarter,
|
|
148
|
+
timeRemaining: scoreUpdate.timeRemaining,
|
|
149
|
+
status: scoreUpdate.isFinished ? 'finished' : 'in-progress',
|
|
150
|
+
lastUpdate: new Date().toISOString()
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
console.log('📊 Score update: ' + scoreUpdate.home + ' - ' + scoreUpdate.away);
|
|
154
|
+
|
|
155
|
+
// Check if game just ended
|
|
156
|
+
if (scoreUpdate.isFinished) {
|
|
157
|
+
console.log('🏁 Game ' + gameId + ' has finished!');
|
|
158
|
+
console.log('Final score: ' + scoreUpdate.home + ' - ' + scoreUpdate.away);
|
|
159
|
+
|
|
160
|
+
// Store final game result
|
|
161
|
+
await Data.create('game-results', {
|
|
162
|
+
gameId: gameId,
|
|
163
|
+
gameName: gameName,
|
|
164
|
+
finalScore: {
|
|
165
|
+
home: scoreUpdate.home,
|
|
166
|
+
away: scoreUpdate.away
|
|
167
|
+
},
|
|
168
|
+
finishedAt: new Date().toISOString()
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// IMPORTANT: Job stops itself when game ends!
|
|
172
|
+
await Jobs.deactivate(job.jobId);
|
|
173
|
+
|
|
174
|
+
console.log('⏹️ Score tracking job stopped for game ' + gameId);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
action: 'game-ended-job-stopped',
|
|
178
|
+
gameId: gameId,
|
|
179
|
+
finalScore: {
|
|
180
|
+
home: scoreUpdate.home,
|
|
181
|
+
away: scoreUpdate.away
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Game still in progress
|
|
187
|
+
return {
|
|
188
|
+
action: 'score-updated',
|
|
189
|
+
gameId: gameId,
|
|
190
|
+
currentScore: {
|
|
191
|
+
home: scoreUpdate.home,
|
|
192
|
+
away: scoreUpdate.away
|
|
193
|
+
},
|
|
194
|
+
quarter: scoreUpdate.quarter,
|
|
195
|
+
timeRemaining: scoreUpdate.timeRemaining
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
console.error('❌ Error updating score for game ' + gameId + ':', error);
|
|
200
|
+
|
|
201
|
+
// Don't stop job on errors - just log and continue
|
|
202
|
+
return {
|
|
203
|
+
action: 'error',
|
|
204
|
+
gameId: gameId,
|
|
205
|
+
error: error.message
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
console.log(`✅ Score tracking job created: ${job.jobId}`);
|
|
212
|
+
console.log(`📊 Job will update scores every ${input.updateIntervalSeconds} seconds`);
|
|
213
|
+
console.log(`⏹️ Job will automatically stop when game ends`);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
game: {
|
|
218
|
+
id: game.id,
|
|
219
|
+
gameId: input.gameId,
|
|
220
|
+
name: input.gameName,
|
|
221
|
+
startTime: input.startTime,
|
|
222
|
+
status: 'scheduled'
|
|
223
|
+
},
|
|
224
|
+
scoreTracker: {
|
|
225
|
+
jobId: job.jobId,
|
|
226
|
+
jobName: job.name,
|
|
227
|
+
updateInterval: input.updateIntervalSeconds,
|
|
228
|
+
startsAt: jobStartTime.toISOString(),
|
|
229
|
+
message: 'Job will automatically stop when game ends'
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get Game Scores Tool
|
|
237
|
+
*
|
|
238
|
+
* Retrieves current scores for a game or all active games
|
|
239
|
+
*/
|
|
240
|
+
export class GetGameScoresTool implements LuaTool {
|
|
241
|
+
name = "get_game_scores";
|
|
242
|
+
description = "Get current scores for tracked games";
|
|
243
|
+
|
|
244
|
+
inputSchema = z.object({
|
|
245
|
+
gameId: z.string().optional()
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
async execute(input: { gameId?: string }) {
|
|
249
|
+
if (input.gameId) {
|
|
250
|
+
// Get specific game
|
|
251
|
+
const games = await Data.get('games', {}, 1, 100);
|
|
252
|
+
const game = games.data?.find((g: any) => g.data.gameId === input.gameId);
|
|
253
|
+
|
|
254
|
+
if (!game) {
|
|
255
|
+
return {
|
|
256
|
+
found: false,
|
|
257
|
+
gameId: input.gameId,
|
|
258
|
+
message: 'Game not found'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
found: true,
|
|
264
|
+
game: {
|
|
265
|
+
gameId: game.data.gameId,
|
|
266
|
+
name: game.data.gameName,
|
|
267
|
+
status: game.data.status,
|
|
268
|
+
currentScore: game.data.currentScore,
|
|
269
|
+
quarter: game.data.quarter,
|
|
270
|
+
timeRemaining: game.data.timeRemaining,
|
|
271
|
+
lastUpdate: game.data.lastUpdate
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get all active games
|
|
277
|
+
const allGames = await Data.get('games', {}, 1, 50);
|
|
278
|
+
|
|
279
|
+
const activeGames = (allGames.data || [])
|
|
280
|
+
.filter((g: any) => g.data.status === 'in-progress' || g.data.status === 'scheduled')
|
|
281
|
+
.map((g: any) => ({
|
|
282
|
+
gameId: g.data.gameId,
|
|
283
|
+
name: g.data.gameName,
|
|
284
|
+
status: g.data.status,
|
|
285
|
+
currentScore: g.data.currentScore,
|
|
286
|
+
lastUpdate: g.data.lastUpdate
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
activeGamesCount: activeGames.length,
|
|
291
|
+
games: activeGames
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Stop Game Tracking Tool
|
|
298
|
+
*
|
|
299
|
+
* Manually stops score tracking for a game
|
|
300
|
+
*/
|
|
301
|
+
export class StopGameTrackingTool implements LuaTool {
|
|
302
|
+
name = "stop_game_tracking";
|
|
303
|
+
description = "Manually stop score tracking for a game";
|
|
304
|
+
|
|
305
|
+
inputSchema = z.object({
|
|
306
|
+
gameId: z.string()
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
async execute(input: { gameId: string }) {
|
|
310
|
+
console.log(`⏹️ Stopping score tracking for game ${input.gameId}...`);
|
|
311
|
+
|
|
312
|
+
// Find the tracking job
|
|
313
|
+
const allJobs = await Jobs.getAll();
|
|
314
|
+
|
|
315
|
+
if (!allJobs.success || !allJobs.data) {
|
|
316
|
+
throw new Error('Failed to fetch jobs');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const trackingJob = allJobs.data.jobs.find((j: any) =>
|
|
320
|
+
j.name === `track-game-${input.gameId}`
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (!trackingJob) {
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
gameId: input.gameId,
|
|
327
|
+
message: 'No tracking job found for this game'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Deactivate the job
|
|
332
|
+
const result = await Jobs.deactivate(trackingJob.id);
|
|
333
|
+
|
|
334
|
+
// Update game status
|
|
335
|
+
const games = await Data.get('games', {}, 1, 100);
|
|
336
|
+
const game = games.data?.find((g: any) => g.data.gameId === input.gameId);
|
|
337
|
+
|
|
338
|
+
if (game) {
|
|
339
|
+
await Data.update('games', game.id, {
|
|
340
|
+
...game.data,
|
|
341
|
+
status: 'stopped',
|
|
342
|
+
stoppedAt: new Date().toISOString()
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(`✅ Score tracking stopped for game ${input.gameId}`);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success: true,
|
|
350
|
+
gameId: input.gameId,
|
|
351
|
+
jobId: trackingJob.id,
|
|
352
|
+
message: 'Score tracking stopped successfully'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Basket Tool Example
|
|
3
|
+
*
|
|
4
|
+
* This tool creates a basket AND schedules a follow-up job to check if it was abandoned.
|
|
5
|
+
* Demonstrates: Dynamic job creation, basket management, and automated follow-ups.
|
|
6
|
+
*
|
|
7
|
+
* Use case: E-commerce abandoned cart recovery
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LuaTool, Baskets, Jobs, Data } from "lua-cli";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
export class CreateSmartBasketTool implements LuaTool {
|
|
14
|
+
name = "create_smart_basket";
|
|
15
|
+
description = "Creates a basket with automatic abandoned cart reminder";
|
|
16
|
+
|
|
17
|
+
inputSchema = z.object({
|
|
18
|
+
currency: z.string().default('USD')
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async execute(input: { currency: string }) {
|
|
22
|
+
console.log('🛒 Creating smart basket with auto-reminder...');
|
|
23
|
+
|
|
24
|
+
// Step 1: Create the basket
|
|
25
|
+
const basketData = {
|
|
26
|
+
currency: input.currency,
|
|
27
|
+
metadata: {
|
|
28
|
+
createdBy: 'smart-basket-tool',
|
|
29
|
+
reminderScheduled: true,
|
|
30
|
+
createdAt: new Date().toISOString()
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const basketInstance = await Baskets.create(basketData);
|
|
35
|
+
const basketInfo = await basketInstance.getData();
|
|
36
|
+
|
|
37
|
+
console.log(`✅ Basket created: ${basketInfo.id}`);
|
|
38
|
+
|
|
39
|
+
// Step 2: Schedule a one-time job to check the basket in 3 hours
|
|
40
|
+
const checkTime = new Date(Date.now() + 3 * 60 * 60 * 1000); // 3 hours from now
|
|
41
|
+
|
|
42
|
+
// Capture basketId in closure for the job
|
|
43
|
+
const basketId = basketInfo.id;
|
|
44
|
+
|
|
45
|
+
// Create a dynamic one-time job using the Jobs API!
|
|
46
|
+
|
|
47
|
+
const job = await Jobs.create({
|
|
48
|
+
name: `check-basket-${basketId}`,
|
|
49
|
+
description: `Check if basket ${basketId} was checked out`,
|
|
50
|
+
|
|
51
|
+
// One-time job that runs in 3 hours
|
|
52
|
+
schedule: {
|
|
53
|
+
type: 'once',
|
|
54
|
+
executeAt: checkTime
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
timeout: 60, // 1 minute timeout
|
|
58
|
+
|
|
59
|
+
// Write normal TypeScript code! It will be compiled automatically
|
|
60
|
+
execute: async () => {
|
|
61
|
+
console.log(`⏰ Checking basket ${basketId}...`);
|
|
62
|
+
|
|
63
|
+
const basketInstance = await Baskets.getById(basketId);
|
|
64
|
+
const basket = await basketInstance.getData();
|
|
65
|
+
|
|
66
|
+
if (basket.status === 'active' || basket.status === 'pending') {
|
|
67
|
+
// Basket is still active - it was abandoned!
|
|
68
|
+
console.log(`🛒 Basket ${basketId} was abandoned`);
|
|
69
|
+
|
|
70
|
+
await Data.create('abandoned-baskets', {
|
|
71
|
+
basketId: basketId,
|
|
72
|
+
currency: basket.currency,
|
|
73
|
+
itemCount: basket.items?.length || 0,
|
|
74
|
+
totalValue: basket.total || 0,
|
|
75
|
+
abandonedAt: new Date().toISOString()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log('📧 Sending abandoned cart reminder email...');
|
|
79
|
+
// TODO: Integrate with email service
|
|
80
|
+
// await sendEmail({
|
|
81
|
+
// to: userEmail,
|
|
82
|
+
// template: 'abandoned-cart',
|
|
83
|
+
// data: { basket }
|
|
84
|
+
// });
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
abandoned: true,
|
|
88
|
+
basketId: basketId,
|
|
89
|
+
reminderSent: true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`✅ Basket ${basketId} was checked out`);
|
|
94
|
+
return {
|
|
95
|
+
abandoned: false,
|
|
96
|
+
basketId: basketId,
|
|
97
|
+
status: basket.status
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(`⏰ One-time job created: ${job.jobId}`);
|
|
103
|
+
console.log(`📅 Will execute at: ${checkTime.toLocaleString()}`);
|
|
104
|
+
|
|
105
|
+
// Return basket info with job details
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
basket: {
|
|
109
|
+
id: basketInfo.id,
|
|
110
|
+
currency: basketInfo.currency,
|
|
111
|
+
status: basketInfo.status
|
|
112
|
+
},
|
|
113
|
+
scheduledJob: {
|
|
114
|
+
jobId: job.jobId,
|
|
115
|
+
name: job.name,
|
|
116
|
+
scheduledFor: checkTime.toISOString(),
|
|
117
|
+
message: 'One-time job will check basket in 3 hours'
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check Abandoned Baskets Tool
|
|
125
|
+
*
|
|
126
|
+
* This tool can be called manually or by a recurring job to check for abandoned baskets.
|
|
127
|
+
*/
|
|
128
|
+
export class CheckAbandonedBasketsTool implements LuaTool {
|
|
129
|
+
name = "check_abandoned_baskets";
|
|
130
|
+
description = "Checks for abandoned baskets and sends reminders";
|
|
131
|
+
|
|
132
|
+
inputSchema = z.object({
|
|
133
|
+
basketId: z.string().optional()
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
async execute(input: { basketId?: string }) {
|
|
137
|
+
console.log('🔍 Checking for abandoned baskets...');
|
|
138
|
+
|
|
139
|
+
// Get all abandoned baskets from data collection
|
|
140
|
+
const abandonedBasketsData = await Data.get('abandoned-baskets', {}, 1, 100);
|
|
141
|
+
const abandonedBaskets = abandonedBasketsData.data || [];
|
|
142
|
+
|
|
143
|
+
if (input.basketId) {
|
|
144
|
+
// Check specific basket
|
|
145
|
+
const specific = abandonedBaskets.find((ab: any) => ab.data.basketId === input.basketId);
|
|
146
|
+
|
|
147
|
+
if (specific) {
|
|
148
|
+
return {
|
|
149
|
+
basketId: input.basketId,
|
|
150
|
+
abandoned: true,
|
|
151
|
+
abandonedAt: specific.data.abandonedAt,
|
|
152
|
+
itemCount: specific.data.itemCount,
|
|
153
|
+
totalValue: specific.data.totalValue
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Not in abandoned list - check current status
|
|
158
|
+
const basketInstance = await Baskets.getById(input.basketId);
|
|
159
|
+
const basket = await basketInstance.getData();
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
basketId: input.basketId,
|
|
163
|
+
abandoned: false,
|
|
164
|
+
status: basket.status,
|
|
165
|
+
itemCount: basket.items?.length || 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Return all abandoned baskets summary
|
|
170
|
+
const summary = abandonedBaskets.map((ab: any) => ({
|
|
171
|
+
basketId: ab.data.basketId,
|
|
172
|
+
currency: ab.data.currency,
|
|
173
|
+
itemCount: ab.data.itemCount,
|
|
174
|
+
totalValue: ab.data.totalValue,
|
|
175
|
+
abandonedAt: ab.data.abandonedAt
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
console.log(`🛒 Found ${abandonedBaskets.length} abandoned baskets`);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
abandonedCount: abandonedBaskets.length,
|
|
183
|
+
abandonedBaskets: summary,
|
|
184
|
+
timestamp: new Date().toISOString()
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Webhook Example
|
|
3
|
+
*
|
|
4
|
+
* This webhook receives payment notifications from payment providers (Stripe, PayPal, etc.).
|
|
5
|
+
* It validates signatures, processes payment events, and updates order status.
|
|
6
|
+
*
|
|
7
|
+
* Common use case: Stripe webhook for payment confirmations
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LuaWebhook, Orders, Data } from "lua-cli";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const paymentWebhook = new LuaWebhook({
|
|
14
|
+
name: "payment-notifications",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
description: "Receives payment notifications from payment providers",
|
|
17
|
+
context: "This webhook processes payment confirmation events from Stripe or other providers. " +
|
|
18
|
+
"It validates the payment, updates order status, and logs the transaction.",
|
|
19
|
+
|
|
20
|
+
// Headers typically include webhook signature for verification
|
|
21
|
+
headerSchema: z.object({
|
|
22
|
+
'stripe-signature': z.string().optional(),
|
|
23
|
+
'content-type': z.string()
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
// Payment event body
|
|
27
|
+
bodySchema: z.object({
|
|
28
|
+
type: z.string(), // e.g., 'payment_intent.succeeded'
|
|
29
|
+
data: z.object({
|
|
30
|
+
object: z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
amount: z.number(),
|
|
33
|
+
currency: z.string(),
|
|
34
|
+
status: z.string(),
|
|
35
|
+
metadata: z.object({
|
|
36
|
+
orderId: z.string().optional(),
|
|
37
|
+
customerId: z.string().optional()
|
|
38
|
+
}).optional()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
execute: async ({ headers, body }) => {
|
|
44
|
+
console.log(`💳 Payment event received:`, body.type);
|
|
45
|
+
|
|
46
|
+
// In production: Verify Stripe signature
|
|
47
|
+
// const signature = headers['stripe-signature'];
|
|
48
|
+
// const isValid = verifyStripeSignature(body, signature);
|
|
49
|
+
// if (!isValid) throw new Error('Invalid signature');
|
|
50
|
+
|
|
51
|
+
const payment = body.data.object;
|
|
52
|
+
const eventType = body.type;
|
|
53
|
+
|
|
54
|
+
// Handle payment success
|
|
55
|
+
if (eventType === 'payment_intent.succeeded') {
|
|
56
|
+
console.log(`✅ Payment successful: ${payment.amount} ${payment.currency}`);
|
|
57
|
+
|
|
58
|
+
// Update order if we have orderId in metadata
|
|
59
|
+
if (payment.metadata?.orderId) {
|
|
60
|
+
try {
|
|
61
|
+
await Orders.updateStatus('confirmed', payment.metadata.orderId);
|
|
62
|
+
console.log(`📦 Order ${payment.metadata.orderId} marked as confirmed`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to update order:', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Log the transaction
|
|
69
|
+
await Data.create('payment-logs', {
|
|
70
|
+
paymentId: payment.id,
|
|
71
|
+
amount: payment.amount,
|
|
72
|
+
currency: payment.currency,
|
|
73
|
+
orderId: payment.metadata?.orderId,
|
|
74
|
+
customerId: payment.metadata?.customerId,
|
|
75
|
+
status: 'succeeded',
|
|
76
|
+
receivedAt: new Date().toISOString()
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
paymentId: payment.id,
|
|
82
|
+
orderId: payment.metadata?.orderId,
|
|
83
|
+
message: 'Payment processed successfully'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle payment failure
|
|
88
|
+
if (eventType === 'payment_intent.payment_failed') {
|
|
89
|
+
console.log(`❌ Payment failed: ${payment.id}`);
|
|
90
|
+
|
|
91
|
+
await Data.create('payment-logs', {
|
|
92
|
+
paymentId: payment.id,
|
|
93
|
+
status: 'failed',
|
|
94
|
+
receivedAt: new Date().toISOString()
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
paymentId: payment.id,
|
|
100
|
+
message: 'Payment failure logged'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Other event types
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
message: `Event ${eventType} received`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export default paymentWebhook;
|
|
113
|
+
|