lua-cli 2.5.7 â 3.0.0-alpha.1
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/agent.api.service.d.ts +45 -0
- package/dist/api/agent.api.service.js +54 -0
- package/dist/api/job.api.service.d.ts +210 -0
- package/dist/api/job.api.service.js +200 -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 +98 -0
- package/dist/api/postprocessor.api.service.js +76 -0
- package/dist/api/preprocessor.api.service.d.ts +98 -0
- package/dist/api/preprocessor.api.service.js +76 -0
- package/dist/api/user.data.api.service.d.ts +28 -0
- package/dist/api/user.data.api.service.js +51 -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 +156 -41
- package/dist/api-exports.js +182 -21
- package/dist/cli/command-definitions.js +149 -7
- package/dist/commands/compile.js +124 -5
- package/dist/commands/completion.d.ts +11 -0
- package/dist/commands/completion.js +209 -0
- package/dist/commands/env.d.ts +3 -2
- package/dist/commands/env.js +42 -17
- package/dist/commands/features.d.ts +16 -0
- package/dist/commands/features.js +352 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -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/persona.d.ts +3 -2
- package/dist/commands/persona.js +43 -18
- 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.d.ts +9 -13
- package/dist/commands/push.js +937 -69
- package/dist/commands/skills.d.ts +16 -0
- package/dist/commands/skills.js +438 -0
- package/dist/commands/test.d.ts +9 -18
- package/dist/commands/test.js +558 -82
- package/dist/commands/webhooks.d.ts +18 -0
- package/dist/commands/webhooks.js +424 -0
- package/dist/common/data.entry.instance.d.ts +7 -0
- package/dist/common/data.entry.instance.js +15 -0
- package/dist/common/job.instance.d.ts +77 -0
- package/dist/common/job.instance.js +108 -0
- package/dist/common/order.instance.d.ts +6 -0
- package/dist/common/order.instance.js +14 -0
- package/dist/common/product.instance.d.ts +6 -0
- package/dist/common/product.instance.js +14 -0
- package/dist/common/user.instance.d.ts +15 -0
- package/dist/common/user.instance.js +38 -0
- package/dist/config/constants.d.ts +2 -2
- package/dist/config/constants.js +4 -4
- package/dist/index.js +14 -3
- package/dist/interfaces/agent.d.ts +33 -1
- package/dist/interfaces/chat.d.ts +22 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.js +7 -0
- package/dist/interfaces/jobs.d.ts +172 -0
- package/dist/interfaces/jobs.js +5 -0
- package/dist/interfaces/message.d.ts +18 -0
- package/dist/interfaces/message.js +1 -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 +14 -0
- package/dist/types/api-contracts.js +0 -7
- 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 +502 -0
- package/dist/types/skill.js +477 -0
- package/dist/utils/agent-management.d.ts +25 -0
- package/dist/utils/agent-management.js +67 -0
- package/dist/utils/bundling.d.ts +31 -1
- package/dist/utils/bundling.js +653 -10
- package/dist/utils/compile.d.ts +63 -0
- package/dist/utils/compile.js +691 -36
- package/dist/utils/deployment.d.ts +2 -1
- package/dist/utils/deployment.js +16 -2
- 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/preprocessor-management.d.ts +9 -0
- package/dist/utils/preprocessor-management.js +118 -0
- package/dist/utils/sandbox.d.ts +61 -1
- package/dist/utils/sandbox.js +283 -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/dist/web/app.css +152 -736
- package/dist/web/app.js +45 -45
- package/package.json +2 -2
- package/template/AGENT_CONFIGURATION.md +251 -0
- package/template/COMPLEX_JOB_EXAMPLES.md +795 -0
- package/template/DYNAMIC_JOB_CREATION.md +371 -0
- package/template/README.md +30 -2
- package/template/WEBHOOKS_JOBS_QUICKSTART.md +318 -0
- package/template/WEBHOOK_JOB_EXAMPLES.md +817 -0
- package/template/package.json +1 -1
- package/template/src/index-agent-example.ts +201 -0
- package/template/src/index.ts +39 -0
- 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/postprocessors/ResponseFormatter.ts +151 -0
- package/template/src/preprocessors/MessageFilter.ts +91 -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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Job Example
|
|
3
|
+
*
|
|
4
|
+
* This job runs every 5 minutes to check system health and alert on issues.
|
|
5
|
+
* Demonstrates: Interval scheduling, monitoring, and alerting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LuaJob, Data, Products } from "lua-cli";
|
|
9
|
+
|
|
10
|
+
const healthCheckJob = new LuaJob({
|
|
11
|
+
name: "health-check",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
description: "System health monitoring",
|
|
14
|
+
context: "Runs every 5 minutes to check API health, database connectivity, and service availability. " +
|
|
15
|
+
"Logs health metrics and can trigger alerts if issues are detected.",
|
|
16
|
+
|
|
17
|
+
// Interval schedule: Every 5 minutes
|
|
18
|
+
schedule: {
|
|
19
|
+
type: 'interval',
|
|
20
|
+
seconds: 300 // 5 minutes = 300 seconds
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Quick timeout for health checks
|
|
24
|
+
timeout: 30,
|
|
25
|
+
|
|
26
|
+
execute: async () => {
|
|
27
|
+
console.log('â¤ī¸ Running health check...');
|
|
28
|
+
const checks: any = {};
|
|
29
|
+
|
|
30
|
+
// Check 1: Database connectivity
|
|
31
|
+
try {
|
|
32
|
+
const testQuery = await Data.get('health-checks', {}, 1, 1);
|
|
33
|
+
checks.database = {
|
|
34
|
+
status: 'healthy',
|
|
35
|
+
responseTime: Date.now()
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
checks.database = {
|
|
39
|
+
status: 'unhealthy',
|
|
40
|
+
error: String(error)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check 2: Products API
|
|
45
|
+
try {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
await Products.get(1, 1);
|
|
48
|
+
const responseTime = Date.now() - startTime;
|
|
49
|
+
|
|
50
|
+
checks.productsApi = {
|
|
51
|
+
status: 'healthy',
|
|
52
|
+
responseTime: `${responseTime}ms`
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
checks.productsApi = {
|
|
56
|
+
status: 'unhealthy',
|
|
57
|
+
error: String(error)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Determine overall health
|
|
62
|
+
const allHealthy = Object.values(checks).every((check: any) => check.status === 'healthy');
|
|
63
|
+
const overallStatus = allHealthy ? 'healthy' : 'degraded';
|
|
64
|
+
|
|
65
|
+
// Log health check result
|
|
66
|
+
await Data.create('health-checks', {
|
|
67
|
+
status: overallStatus,
|
|
68
|
+
checks,
|
|
69
|
+
timestamp: new Date().toISOString()
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Alert if unhealthy (in production, send to monitoring service)
|
|
73
|
+
if (!allHealthy) {
|
|
74
|
+
console.warn('â ī¸ System health degraded:', checks);
|
|
75
|
+
// TODO: Send alert to monitoring service
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: overallStatus,
|
|
80
|
+
checks,
|
|
81
|
+
timestamp: new Date().toISOString()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export default healthCheckJob;
|
|
87
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Formatter PostProcessor
|
|
3
|
+
*
|
|
4
|
+
* PostProcessors run AFTER the AI agent generates a response.
|
|
5
|
+
* They can:
|
|
6
|
+
* - Format or beautify responses
|
|
7
|
+
* - Add branding or signatures
|
|
8
|
+
* - Translate responses
|
|
9
|
+
* - Add disclaimers
|
|
10
|
+
* - Filter sensitive information
|
|
11
|
+
* - Adapt tone or style
|
|
12
|
+
*
|
|
13
|
+
* The execute function receives:
|
|
14
|
+
* - user: UserDataInstance (current user info)
|
|
15
|
+
* - message: string (the user's original message)
|
|
16
|
+
* - response: string (the agent's generated response)
|
|
17
|
+
* - channel: string (where the response will be sent)
|
|
18
|
+
*
|
|
19
|
+
* Returns: The formatted response that will be sent to the user
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { PostProcessor } from "lua-cli";
|
|
23
|
+
|
|
24
|
+
const responseFormatter = new PostProcessor({
|
|
25
|
+
name: "response-formatter",
|
|
26
|
+
version: "1.0.0",
|
|
27
|
+
description: "Formats and enhances agent responses before sending to users",
|
|
28
|
+
context: "This postprocessor adds branding, formats responses for different channels, and ensures consistency.",
|
|
29
|
+
|
|
30
|
+
execute: async (user, message, response, channel) => {
|
|
31
|
+
console.log(`[ResponseFormatter] Formatting response for user ${user.id} via ${channel}`);
|
|
32
|
+
|
|
33
|
+
let formattedResponse = response;
|
|
34
|
+
|
|
35
|
+
// Example 1: Add personalized greeting for first-time users
|
|
36
|
+
if (user.metadata?.isFirstMessage) {
|
|
37
|
+
formattedResponse = `đ Welcome, ${user.name || 'there'}! ${formattedResponse}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Example 2: Format based on channel
|
|
41
|
+
switch (channel.toLowerCase()) {
|
|
42
|
+
case 'sms':
|
|
43
|
+
// SMS: Keep it short, remove emojis, add link to full response
|
|
44
|
+
formattedResponse = formattedResponse
|
|
45
|
+
.replace(/[^\w\s.,!?-]/g, '') // Remove special chars
|
|
46
|
+
.substring(0, 160); // SMS limit
|
|
47
|
+
if (response.length > 160) {
|
|
48
|
+
formattedResponse += '... (continued at app.example.com)';
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'email':
|
|
53
|
+
// Email: Add proper formatting and signature
|
|
54
|
+
formattedResponse = `
|
|
55
|
+
<html>
|
|
56
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6;">
|
|
57
|
+
<p>Hi ${user.name || 'there'},</p>
|
|
58
|
+
<p>${formattedResponse.replace(/\n/g, '<br>')}</p>
|
|
59
|
+
<br>
|
|
60
|
+
<p>Best regards,<br>
|
|
61
|
+
<strong>Zara</strong><br>
|
|
62
|
+
<em>Your E-commerce Assistant</em></p>
|
|
63
|
+
<hr>
|
|
64
|
+
<p style="font-size: 0.8em; color: #666;">
|
|
65
|
+
This is an automated response. Reply to this email if you need further assistance.
|
|
66
|
+
</p>
|
|
67
|
+
</body>
|
|
68
|
+
</html>`;
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'slack':
|
|
72
|
+
case 'discord':
|
|
73
|
+
// Chat platforms: Add markdown formatting
|
|
74
|
+
formattedResponse = formattedResponse
|
|
75
|
+
.replace(/\*\*(.*?)\*\*/g, '*$1*') // Bold
|
|
76
|
+
.replace(/__(.*?)__/g, '_$1_'); // Italic
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
// Default: Clean formatting
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Example 3: Add product links automatically
|
|
85
|
+
// Detect when products are mentioned and add quick links
|
|
86
|
+
const productMentionPattern = /(?:product|item|sku)[\s:]+(\w+)/gi;
|
|
87
|
+
const matches = formattedResponse.matchAll(productMentionPattern);
|
|
88
|
+
|
|
89
|
+
for (const match of matches) {
|
|
90
|
+
const productId = match[1];
|
|
91
|
+
// You could look up actual product URLs here
|
|
92
|
+
formattedResponse = formattedResponse.replace(
|
|
93
|
+
match[0],
|
|
94
|
+
`${match[0]} [View](https://example.com/products/${productId})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Example 4: Add helpful footer based on context
|
|
99
|
+
const hasOrderMention = /order|purchase|buy/i.test(message);
|
|
100
|
+
const hasBasketMention = /basket|cart|checkout/i.test(message);
|
|
101
|
+
|
|
102
|
+
if (hasOrderMention) {
|
|
103
|
+
formattedResponse += '\n\nđĄ Tip: You can track your orders anytime by asking "show my orders"';
|
|
104
|
+
} else if (hasBasketMention) {
|
|
105
|
+
formattedResponse += '\n\nđ Need help? Just ask me to "show my basket" or "checkout"';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Example 5: Filter sensitive information
|
|
109
|
+
// Remove any accidentally exposed sensitive data
|
|
110
|
+
formattedResponse = formattedResponse
|
|
111
|
+
.replace(/\b\d{16}\b/g, '****-****-****-****') // Credit card numbers
|
|
112
|
+
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '***-**-****') // SSN
|
|
113
|
+
.replace(/password[:\s]+\S+/gi, 'password: [REDACTED]'); // Passwords
|
|
114
|
+
|
|
115
|
+
// Example 6: Add emoji based on sentiment (simple version)
|
|
116
|
+
if (/thank|great|awesome|love|perfect/i.test(message)) {
|
|
117
|
+
formattedResponse = `đ ${formattedResponse}`;
|
|
118
|
+
} else if (/problem|issue|error|wrong|broken/i.test(message)) {
|
|
119
|
+
formattedResponse = `đ§ ${formattedResponse}`;
|
|
120
|
+
} else if (/help|how|what|where/i.test(message)) {
|
|
121
|
+
formattedResponse = `đĄ ${formattedResponse}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Example 7: Add timestamp for long responses
|
|
125
|
+
if (formattedResponse.length > 500) {
|
|
126
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
127
|
+
formattedResponse += `\n\n_Response generated at ${timestamp}_`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Example 8: Add disclaimer for legal/financial advice
|
|
131
|
+
const needsDisclaimer = /legal|law|lawsuit|sue|invest|stock|crypto|tax/i.test(message);
|
|
132
|
+
if (needsDisclaimer) {
|
|
133
|
+
formattedResponse += '\n\nâ ī¸ _Disclaimer: This information is for general guidance only and should not be considered professional legal or financial advice. Please consult with a qualified professional for your specific situation._';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Example 9: Ensure proper length
|
|
137
|
+
const MAX_LENGTH = 4000; // Most platforms have limits
|
|
138
|
+
if (formattedResponse.length > MAX_LENGTH) {
|
|
139
|
+
formattedResponse = formattedResponse.substring(0, MAX_LENGTH - 50) +
|
|
140
|
+
'...\n\n_Response truncated. Please ask for more specific information._';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`[ResponseFormatter] Original length: ${response.length} chars`);
|
|
144
|
+
console.log(`[ResponseFormatter] Formatted length: ${formattedResponse.length} chars`);
|
|
145
|
+
|
|
146
|
+
return formattedResponse;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export default responseFormatter;
|
|
151
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Filter PreProcessor
|
|
3
|
+
*
|
|
4
|
+
* PreProcessors run BEFORE messages reach the AI agent.
|
|
5
|
+
* They can:
|
|
6
|
+
* - Filter out spam or inappropriate content
|
|
7
|
+
* - Translate messages
|
|
8
|
+
* - Add context to messages
|
|
9
|
+
* - Modify user input
|
|
10
|
+
* - Block certain types of requests
|
|
11
|
+
*
|
|
12
|
+
* The execute function receives:
|
|
13
|
+
* - user: UserDataInstance (current user info)
|
|
14
|
+
* - message: string (the user's message)
|
|
15
|
+
* - channel: string (where the message came from)
|
|
16
|
+
*
|
|
17
|
+
* Returns: The potentially modified message that will be sent to the agent
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { PreProcessor } from "lua-cli";
|
|
21
|
+
|
|
22
|
+
const messageFilter = new PreProcessor({
|
|
23
|
+
name: "message-filter",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
description: "Filters and preprocesses user messages before they reach the agent",
|
|
26
|
+
context: "This preprocessor removes spam, blocks inappropriate content, and adds helpful context to user messages.",
|
|
27
|
+
|
|
28
|
+
execute: async (user, message, channel) => {
|
|
29
|
+
console.log(`[MessageFilter] Processing message from user ${user.id} via ${channel}`);
|
|
30
|
+
|
|
31
|
+
// Example 1: Block spam patterns
|
|
32
|
+
const spamPatterns = [
|
|
33
|
+
/buy now/i,
|
|
34
|
+
/click here/i,
|
|
35
|
+
/free money/i,
|
|
36
|
+
/\b(viagra|cialis)\b/i
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const pattern of spamPatterns) {
|
|
40
|
+
if (pattern.test(message)) {
|
|
41
|
+
console.log('[MessageFilter] Spam detected, blocking message');
|
|
42
|
+
return "[This message was blocked by spam filter]";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Example 2: Remove excessive punctuation
|
|
47
|
+
let filteredMessage = message.replace(/([!?.]){3,}/g, '$1$1'); // Reduce !!!!! to !!
|
|
48
|
+
|
|
49
|
+
// Example 3: Normalize whitespace
|
|
50
|
+
filteredMessage = filteredMessage.trim().replace(/\s+/g, ' ');
|
|
51
|
+
|
|
52
|
+
// Example 4: Add user context for the agent
|
|
53
|
+
// This helps the agent understand who they're talking to
|
|
54
|
+
if (user.name && !filteredMessage.toLowerCase().includes('my name')) {
|
|
55
|
+
// Agent will have context about user's name without being intrusive
|
|
56
|
+
filteredMessage = `[User: ${user.name}] ${filteredMessage}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Example 5: Detect and flag urgent messages
|
|
60
|
+
const urgentKeywords = ['urgent', 'emergency', 'asap', 'immediately', 'help'];
|
|
61
|
+
const isUrgent = urgentKeywords.some(keyword =>
|
|
62
|
+
filteredMessage.toLowerCase().includes(keyword)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (isUrgent) {
|
|
66
|
+
filteredMessage = `[URGENT] ${filteredMessage}`;
|
|
67
|
+
console.log('[MessageFilter] Urgent message flagged');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Example 6: Add channel context
|
|
71
|
+
// This helps the agent adapt its response based on the channel
|
|
72
|
+
if (channel && channel !== 'default') {
|
|
73
|
+
filteredMessage = `[Via ${channel}] ${filteredMessage}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Example 7: Language detection hint (optional)
|
|
77
|
+
// You could integrate with a language detection library here
|
|
78
|
+
const hasNonEnglish = /[^\u0000-\u007F]/.test(message);
|
|
79
|
+
if (hasNonEnglish) {
|
|
80
|
+
filteredMessage = `[Contains non-ASCII characters] ${filteredMessage}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`[MessageFilter] Original: "${message}"`);
|
|
84
|
+
console.log(`[MessageFilter] Filtered: "${filteredMessage}"`);
|
|
85
|
+
|
|
86
|
+
return filteredMessage;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export default messageFilter;
|
|
91
|
+
|
|
@@ -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
|
+
|