lua-cli 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Resources Command
3
+ * Manages agent resources (knowledge base documents)
4
+ */
5
+ import { loadApiKey, checkApiKey } from '../services/auth.js';
6
+ import { readSkillConfig } from '../utils/files.js';
7
+ import { withErrorHandling, writeProgress, writeSuccess } from '../utils/cli.js';
8
+ import { BASE_URLS } from '../config/constants.js';
9
+ import { safePrompt } from '../utils/prompt-handler.js';
10
+ import { validateConfig, validateAgentConfig, } from '../utils/dev-helpers.js';
11
+ /**
12
+ * Main resources command - manages agent resources
13
+ *
14
+ * Features:
15
+ * - List all resources
16
+ * - Create new resources
17
+ * - Update existing resources
18
+ * - Delete resources
19
+ * - View resource content
20
+ *
21
+ * @returns Promise that resolves when command completes
22
+ */
23
+ export async function resourcesCommand() {
24
+ return withErrorHandling(async () => {
25
+ // Step 1: Load configuration
26
+ const config = readSkillConfig();
27
+ validateConfig(config);
28
+ validateAgentConfig(config);
29
+ const agentId = config.agent.agentId;
30
+ // Step 2: Authenticate
31
+ const apiKey = await loadApiKey();
32
+ if (!apiKey) {
33
+ console.error("❌ No API key found. Please run 'lua auth configure' to set up your API key.");
34
+ process.exit(1);
35
+ }
36
+ await checkApiKey(apiKey);
37
+ writeProgress("✅ Authenticated");
38
+ const context = {
39
+ agentId,
40
+ apiKey,
41
+ };
42
+ // Step 3: Start management loop
43
+ await manageResources(context);
44
+ }, "resources");
45
+ }
46
+ /**
47
+ * Main resources management loop
48
+ */
49
+ async function manageResources(context) {
50
+ let continueManaging = true;
51
+ while (continueManaging) {
52
+ // Load current resources
53
+ const resources = await loadResources(context);
54
+ console.log("\n" + "=".repeat(60));
55
+ console.log("📚 Agent Resources");
56
+ console.log("=".repeat(60) + "\n");
57
+ if (resources.length === 0) {
58
+ console.log("ℹ️ No resources configured.\n");
59
+ console.log("💡 Resources are knowledge base documents your agent can reference.\n");
60
+ }
61
+ else {
62
+ resources.forEach((resource, index) => {
63
+ const preview = resource.content.length > 60
64
+ ? resource.content.substring(0, 60) + '...'
65
+ : resource.content;
66
+ console.log(`${index + 1}. ${resource.name}`);
67
+ console.log(` Preview: ${preview}`);
68
+ console.log();
69
+ });
70
+ }
71
+ const actionAnswer = await safePrompt([
72
+ {
73
+ type: 'list',
74
+ name: 'action',
75
+ message: 'What would you like to do?',
76
+ choices: [
77
+ { name: '➕ Create new resource', value: 'create' },
78
+ { name: '✏️ Update existing resource', value: 'update' },
79
+ { name: '🗑️ Delete resource', value: 'delete' },
80
+ { name: '👁️ View resource content', value: 'view' },
81
+ { name: '🔄 Refresh list', value: 'refresh' },
82
+ { name: '❌ Exit', value: 'exit' }
83
+ ]
84
+ }
85
+ ]);
86
+ if (!actionAnswer)
87
+ return;
88
+ const { action } = actionAnswer;
89
+ switch (action) {
90
+ case 'create':
91
+ await createResource(context);
92
+ break;
93
+ case 'update':
94
+ await updateResource(context, resources);
95
+ break;
96
+ case 'delete':
97
+ await deleteResource(context, resources);
98
+ break;
99
+ case 'view':
100
+ await viewResource(resources);
101
+ break;
102
+ case 'refresh':
103
+ // Just loop again
104
+ break;
105
+ case 'exit':
106
+ continueManaging = false;
107
+ console.log("\n👋 Goodbye!\n");
108
+ break;
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Load resources from API
114
+ */
115
+ async function loadResources(context) {
116
+ try {
117
+ const response = await fetch(`${BASE_URLS.API}/admin/agents/${context.agentId}/resources`, {
118
+ method: 'GET',
119
+ headers: {
120
+ 'accept': 'application/json',
121
+ 'Authorization': `Bearer ${context.apiKey}`
122
+ }
123
+ });
124
+ if (!response.ok) {
125
+ return [];
126
+ }
127
+ const data = await response.json();
128
+ if (Array.isArray(data)) {
129
+ return data;
130
+ }
131
+ else if (data.data && Array.isArray(data.data)) {
132
+ return data.data;
133
+ }
134
+ return [];
135
+ }
136
+ catch (error) {
137
+ console.error('⚠️ Error loading resources');
138
+ return [];
139
+ }
140
+ }
141
+ /**
142
+ * Create a new resource
143
+ */
144
+ async function createResource(context) {
145
+ const answers = await safePrompt([
146
+ {
147
+ type: 'input',
148
+ name: 'name',
149
+ message: 'Resource name:',
150
+ validate: (input) => {
151
+ if (!input.trim())
152
+ return 'Resource name is required';
153
+ return true;
154
+ }
155
+ },
156
+ {
157
+ type: 'editor',
158
+ name: 'content',
159
+ message: 'Resource content (will open in your default editor):',
160
+ validate: (input) => {
161
+ if (!input.trim())
162
+ return 'Resource content cannot be empty';
163
+ return true;
164
+ }
165
+ }
166
+ ]);
167
+ if (!answers)
168
+ return;
169
+ writeProgress("🔄 Creating resource...");
170
+ try {
171
+ const response = await fetch(`${BASE_URLS.API}/admin/agents/${context.agentId}/resources`, {
172
+ method: 'POST',
173
+ headers: {
174
+ 'accept': 'application/json',
175
+ 'Authorization': `Bearer ${context.apiKey}`,
176
+ 'Content-Type': 'application/json'
177
+ },
178
+ body: JSON.stringify({
179
+ name: answers.name.trim(),
180
+ content: answers.content.trim()
181
+ })
182
+ });
183
+ if (!response.ok) {
184
+ const errorText = await response.text();
185
+ throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
186
+ }
187
+ writeSuccess(`✅ Resource "${answers.name}" created successfully`);
188
+ }
189
+ catch (error) {
190
+ console.error('❌ Error creating resource:', error);
191
+ }
192
+ }
193
+ /**
194
+ * Update an existing resource
195
+ */
196
+ async function updateResource(context, resources) {
197
+ if (resources.length === 0) {
198
+ console.log("\nℹ️ No resources to update.\n");
199
+ return;
200
+ }
201
+ const selectAnswer = await safePrompt([
202
+ {
203
+ type: 'list',
204
+ name: 'selectedId',
205
+ message: 'Select resource to update:',
206
+ choices: resources.map(r => ({
207
+ name: r.name,
208
+ value: r.id
209
+ }))
210
+ }
211
+ ]);
212
+ if (!selectAnswer)
213
+ return;
214
+ const selectedResource = resources.find(r => r.id === selectAnswer.selectedId);
215
+ const updateAnswers = await safePrompt([
216
+ {
217
+ type: 'input',
218
+ name: 'name',
219
+ message: 'Resource name:',
220
+ default: selectedResource.name,
221
+ validate: (input) => {
222
+ if (!input.trim())
223
+ return 'Resource name is required';
224
+ return true;
225
+ }
226
+ },
227
+ {
228
+ type: 'editor',
229
+ name: 'content',
230
+ message: 'Resource content (will open in your default editor):',
231
+ default: selectedResource.content,
232
+ validate: (input) => {
233
+ if (!input.trim())
234
+ return 'Resource content cannot be empty';
235
+ return true;
236
+ }
237
+ }
238
+ ]);
239
+ if (!updateAnswers)
240
+ return;
241
+ writeProgress("🔄 Updating resource...");
242
+ try {
243
+ const response = await fetch(`${BASE_URLS.API}/admin/agents/${context.agentId}/resources/${selectAnswer.selectedId}`, {
244
+ method: 'PUT',
245
+ headers: {
246
+ 'accept': 'application/json',
247
+ 'Authorization': `Bearer ${context.apiKey}`,
248
+ 'Content-Type': 'application/json'
249
+ },
250
+ body: JSON.stringify({
251
+ name: updateAnswers.name.trim(),
252
+ content: updateAnswers.content.trim()
253
+ })
254
+ });
255
+ if (!response.ok) {
256
+ const errorText = await response.text();
257
+ throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
258
+ }
259
+ writeSuccess(`✅ Resource "${updateAnswers.name}" updated successfully`);
260
+ }
261
+ catch (error) {
262
+ console.error('❌ Error updating resource:', error);
263
+ }
264
+ }
265
+ /**
266
+ * Delete a resource
267
+ */
268
+ async function deleteResource(context, resources) {
269
+ if (resources.length === 0) {
270
+ console.log("\nℹ️ No resources to delete.\n");
271
+ return;
272
+ }
273
+ const selectAnswer = await safePrompt([
274
+ {
275
+ type: 'list',
276
+ name: 'selectedId',
277
+ message: 'Select resource to delete:',
278
+ choices: resources.map(r => ({
279
+ name: r.name,
280
+ value: r.id
281
+ }))
282
+ }
283
+ ]);
284
+ if (!selectAnswer)
285
+ return;
286
+ const selectedResource = resources.find(r => r.id === selectAnswer.selectedId);
287
+ const confirmAnswer = await safePrompt([
288
+ {
289
+ type: 'confirm',
290
+ name: 'confirm',
291
+ message: `Are you sure you want to delete "${selectedResource.name}"?`,
292
+ default: false
293
+ }
294
+ ]);
295
+ if (!confirmAnswer || !confirmAnswer.confirm) {
296
+ console.log("\nℹ️ Deletion cancelled.\n");
297
+ return;
298
+ }
299
+ writeProgress("🔄 Deleting resource...");
300
+ try {
301
+ const response = await fetch(`${BASE_URLS.API}/admin/agents/${context.agentId}/resources/${selectAnswer.selectedId}`, {
302
+ method: 'DELETE',
303
+ headers: {
304
+ 'accept': '*/*',
305
+ 'Authorization': `Bearer ${context.apiKey}`
306
+ }
307
+ });
308
+ if (!response.ok) {
309
+ const errorText = await response.text();
310
+ throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
311
+ }
312
+ writeSuccess(`✅ Resource "${selectedResource.name}" deleted successfully`);
313
+ }
314
+ catch (error) {
315
+ console.error('❌ Error deleting resource:', error);
316
+ }
317
+ }
318
+ /**
319
+ * View a resource's full content
320
+ */
321
+ async function viewResource(resources) {
322
+ if (resources.length === 0) {
323
+ console.log("\nℹ️ No resources to view.\n");
324
+ return;
325
+ }
326
+ const selectAnswer = await safePrompt([
327
+ {
328
+ type: 'list',
329
+ name: 'selectedId',
330
+ message: 'Select resource to view:',
331
+ choices: resources.map(r => ({
332
+ name: r.name,
333
+ value: r.id
334
+ }))
335
+ }
336
+ ]);
337
+ if (!selectAnswer)
338
+ return;
339
+ const resource = resources.find(r => r.id === selectAnswer.selectedId);
340
+ console.log("\n" + "=".repeat(60));
341
+ console.log(`📄 Resource: ${resource.name}`);
342
+ console.log("=".repeat(60));
343
+ if (resource.createdAt) {
344
+ const date = new Date(resource.createdAt);
345
+ console.log(`Created: ${date.toLocaleString()}`);
346
+ }
347
+ if (resource.updatedAt) {
348
+ const date = new Date(resource.updatedAt);
349
+ console.log(`Updated: ${date.toLocaleString()}`);
350
+ }
351
+ console.log("=".repeat(60) + "\n");
352
+ console.log(resource.content);
353
+ console.log("\n" + "=".repeat(60) + "\n");
354
+ await safePrompt([
355
+ {
356
+ type: 'input',
357
+ name: 'continue',
358
+ message: 'Press Enter to continue...'
359
+ }
360
+ ]);
361
+ }
@@ -29,7 +29,9 @@ export default class DataEntryInstance {
29
29
  toJSON() {
30
30
  return {
31
31
  ...this.data,
32
- score: this.score
32
+ score: this.score,
33
+ id: this.id,
34
+ collectionName: this.collectionName
33
35
  };
34
36
  }
35
37
  /**
@@ -39,7 +41,9 @@ export default class DataEntryInstance {
39
41
  [Symbol.for('nodejs.util.inspect.custom')]() {
40
42
  return {
41
43
  ...this.data,
42
- score: this.score
44
+ score: this.score,
45
+ id: this.id,
46
+ collectionName: this.collectionName
43
47
  };
44
48
  }
45
49
  /**
@@ -33,10 +33,11 @@ export declare const SKILL_DEFAULTS: {
33
33
  };
34
34
  /**
35
35
  * External packages that should not be bundled with tools
36
- * These are either lua-cli modules, native modules, or modules
37
- * that should be provided by the runtime environment
36
+ * These are native modules, or modules that should be provided by the runtime environment
37
+ * Note: lua-cli is NOT in this list because it's handled by the sandboxGlobalsPlugin
38
+ * which transforms imports to use sandbox globals instead
38
39
  */
39
- export declare const EXTERNAL_PACKAGES: readonly ["lua-cli/skill", "lua-cli", "lua-cli/user-data-api", "lua-cli/product-api", "lua-cli/custom-data-api", "zod", "keytar", "esbuild", "commander", "inquirer", "node-fetch", "ws", "socket.io-client", "ts-morph"];
40
+ export declare const EXTERNAL_PACKAGES: readonly ["zod", "keytar", "esbuild", "commander", "inquirer", "node-fetch", "ws", "socket.io-client", "ts-morph"];
40
41
  /**
41
42
  * Common esbuild configuration for tool bundling
42
43
  */
@@ -32,15 +32,11 @@ export const SKILL_DEFAULTS = {
32
32
  };
33
33
  /**
34
34
  * External packages that should not be bundled with tools
35
- * These are either lua-cli modules, native modules, or modules
36
- * that should be provided by the runtime environment
35
+ * These are native modules, or modules that should be provided by the runtime environment
36
+ * Note: lua-cli is NOT in this list because it's handled by the sandboxGlobalsPlugin
37
+ * which transforms imports to use sandbox globals instead
37
38
  */
38
39
  export const EXTERNAL_PACKAGES = [
39
- 'lua-cli/skill',
40
- 'lua-cli',
41
- 'lua-cli/user-data-api',
42
- 'lua-cli/product-api',
43
- 'lua-cli/custom-data-api',
44
40
  'zod',
45
41
  'keytar',
46
42
  'esbuild',
@@ -1,5 +1,11 @@
1
+ /**
2
+ * Base URLs for the API, Auth, and Chat - Production
3
+ */
1
4
  export declare const BASE_URLS: {
2
5
  readonly API: "https://api.heylua.ai";
3
6
  readonly AUTH: "https://auth.heylua.ai";
4
7
  readonly CHAT: "https://api.heylua.ai";
5
8
  };
9
+ /**
10
+ * Base URLs for the API, Auth, and Chat - Development
11
+ */
@@ -1,5 +1,16 @@
1
+ /**
2
+ * Base URLs for the API, Auth, and Chat - Production
3
+ */
1
4
  export const BASE_URLS = {
2
5
  API: 'https://api.heylua.ai',
3
6
  AUTH: 'https://auth.heylua.ai',
4
7
  CHAT: 'https://api.heylua.ai',
5
8
  };
9
+ /**
10
+ * Base URLs for the API, Auth, and Chat - Development
11
+ */
12
+ // export const BASE_URLS = {
13
+ // API: 'http://localhost:3022',
14
+ // AUTH: 'https://auth.heylua.ai',
15
+ // CHAT: 'http://localhost:3001',
16
+ // } as const;
package/dist/index.d.ts CHANGED
@@ -6,15 +6,6 @@
6
6
  * Provides tools for authentication, project initialization, compilation,
7
7
  * testing, deployment, and development.
8
8
  *
9
- * Usage:
10
- * lua auth configure # Set up authentication
11
- * lua init # Initialize new project
12
- * lua compile # Compile skill
13
- * lua test # Test tools
14
- * lua push # Push to server
15
- * lua deploy # Deploy to production
16
- * lua dev # Start development mode
17
- *
18
9
  * For more information: https://docs.heylua.ai
19
10
  */
20
11
  export {};
package/dist/index.js CHANGED
@@ -6,15 +6,6 @@
6
6
  * Provides tools for authentication, project initialization, compilation,
7
7
  * testing, deployment, and development.
8
8
  *
9
- * Usage:
10
- * lua auth configure # Set up authentication
11
- * lua init # Initialize new project
12
- * lua compile # Compile skill
13
- * lua test # Test tools
14
- * lua push # Push to server
15
- * lua deploy # Deploy to production
16
- * lua dev # Start development mode
17
- *
18
9
  * For more information: https://docs.heylua.ai
19
10
  */
20
11
  import { Command } from "commander";
@@ -24,8 +15,36 @@ const program = new Command();
24
15
  // Configure program metadata
25
16
  program
26
17
  .name("lua")
27
- .description("Lua AI Skill Management CLI")
28
- .version("2.3.1");
18
+ .description("Lua AI - Build and deploy AI agents with superpowers")
19
+ .version("2.4.0")
20
+ .addHelpText('before', `
21
+ ------------------------------------------------------------------
22
+ Lua AI CLI v2.4.0 - Build and deploy AI agents with superpowers
23
+ ------------------------------------------------------------------
24
+ `)
25
+ .addHelpText('after', `
26
+ Categories:
27
+ 🔐 Authentication Manage API keys and authentication
28
+ 🚀 Project Setup Initialize and configure projects
29
+ 📦 Development Build and test your skills locally
30
+ ☁️ Deployment Push and deploy to production
31
+ 💬 Testing Chat and test your agent
32
+ ⚙️ Configuration Manage environment and persona
33
+
34
+ Examples:
35
+ $ lua auth configure 🔑 Set up your API key
36
+ $ lua init 🚀 Initialize a new project
37
+ $ lua compile 📦 Compile your skills
38
+ $ lua test 🧪 Test tools interactively
39
+ $ lua push ☁️ Push to server
40
+ $ lua deploy 🚀 Deploy to production
41
+ $ lua chat 💬 Start interactive chat
42
+ $ lua env ⚙️ Manage environment variables
43
+ $ lua persona 🤖 Manage agent persona
44
+
45
+ 🌙 Documentation: https://docs.heylua.ai
46
+ 🌙 Support: https://heylua.ai/support
47
+ `);
29
48
  // Set up all command groups
30
49
  setupAuthCommands(program);
31
50
  setupSkillCommands(program);
@@ -8,6 +8,66 @@ import { build } from "esbuild";
8
8
  import { writeProgress } from "./cli.js";
9
9
  import { COMPILE_DIRS, COMPILE_FILES, ESBUILD_TOOL_CONFIG, ESBUILD_INDEX_CONFIG, DEFAULT_INPUT_SCHEMA, } from '../config/compile.constants.js';
10
10
  import { wrapToolForVM, createExecuteFunction, evaluateZodSchemaToJsonSchema, } from './compile.js';
11
+ /**
12
+ * esbuild plugin to inject sandbox globals instead of requiring lua-cli
13
+ * This removes require("lua-cli") statements and injects the global API objects
14
+ * that are available in the sandbox (Product, User, Data, Baskets, Order)
15
+ */
16
+ const sandboxGlobalsPlugin = {
17
+ name: 'sandbox-globals',
18
+ setup(build) {
19
+ // Only process user files, not node_modules
20
+ build.onLoad({ filter: /\.([jt]sx?)$/, namespace: 'file' }, async (args) => {
21
+ // Skip node_modules
22
+ if (args.path.includes('node_modules')) {
23
+ return null;
24
+ }
25
+ const contents = await fs.promises.readFile(args.path, 'utf8');
26
+ // Only transform files that import from lua-cli
27
+ if (!contents.includes('lua-cli')) {
28
+ return null;
29
+ }
30
+ // Replace lua-cli imports with global references
31
+ let transformedContents = contents;
32
+ // Replace named imports from lua-cli
33
+ // Match: import { Products, User, Data, etc. } from "lua-cli"
34
+ transformedContents = transformedContents.replace(/import\s+{([^}]+)}\s+from\s+["']lua-cli["'];?/g, (match, imports) => {
35
+ // Just remove the import, globals will be available in sandbox
36
+ return '// lua-cli imports removed - using sandbox globals';
37
+ });
38
+ // Replace lua-cli/skill imports - keep env and LuaTool as they might be needed
39
+ transformedContents = transformedContents.replace(/import\s+{([^}]+)}\s+from\s+["']lua-cli\/skill["'];?/g, (match, imports) => {
40
+ // Check if env is imported, if so we need to keep a reference
41
+ if (imports.includes('env')) {
42
+ return '// lua-cli/skill imports removed - env available in sandbox';
43
+ }
44
+ return '// lua-cli/skill imports removed - using sandbox globals';
45
+ });
46
+ // Map import names to sandbox global names
47
+ // Products -> Product, Orders -> Order, etc.
48
+ const globalMappings = {
49
+ 'Products': 'Products',
50
+ 'User': 'User',
51
+ 'Data': 'Data',
52
+ 'Baskets': 'Baskets',
53
+ 'Orders': 'Orders'
54
+ };
55
+ // Replace usage of imported names with globals
56
+ for (const [importName, globalName] of Object.entries(globalMappings)) {
57
+ // Replace standalone usage (e.g., Products.method() -> Product.method())
58
+ const regex = new RegExp(`\\b${importName}\\.`, 'g');
59
+ transformedContents = transformedContents.replace(regex, `${globalName}.`);
60
+ }
61
+ // Note: env function and LuaTool interface are already available in sandbox
62
+ // env is a function in the sandbox context (see sandbox.ts line 333)
63
+ return {
64
+ contents: transformedContents,
65
+ loader: args.path.endsWith('.ts') ? 'ts' :
66
+ args.path.endsWith('.tsx') ? 'tsx' : 'js'
67
+ };
68
+ });
69
+ },
70
+ };
11
71
  /**
12
72
  * Bundles a tool's TypeScript code into a standalone JavaScript file.
13
73
  * Uses esbuild to:
@@ -26,6 +86,7 @@ export async function bundleTool(tool, distDir) {
26
86
  ...ESBUILD_TOOL_CONFIG,
27
87
  entryPoints: [tool.filePath],
28
88
  outfile: outputPath,
89
+ plugins: [sandboxGlobalsPlugin],
29
90
  });
30
91
  // Wrap the bundled code for VM execution environment
31
92
  await wrapToolForVM(outputPath, tool);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Prompt Handler Utility
3
+ * Gracefully handles user interruptions (Ctrl+C) in inquirer prompts
4
+ */
5
+ /**
6
+ * Wraps inquirer.prompt to gracefully handle Ctrl+C interruptions
7
+ */
8
+ export declare function safePrompt<T = any>(questions: any): Promise<T | null>;
9
+ /**
10
+ * Setup global SIGINT handler for graceful exits
11
+ */
12
+ export declare function setupGlobalInterruptHandler(): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Prompt Handler Utility
3
+ * Gracefully handles user interruptions (Ctrl+C) in inquirer prompts
4
+ */
5
+ import inquirer from 'inquirer';
6
+ /**
7
+ * Wraps inquirer.prompt to gracefully handle Ctrl+C interruptions
8
+ */
9
+ export async function safePrompt(questions) {
10
+ try {
11
+ const answers = await inquirer.prompt(questions);
12
+ return answers;
13
+ }
14
+ catch (error) {
15
+ // Handle Ctrl+C gracefully
16
+ if (error.name === 'ExitPromptError' || error.message?.includes('SIGINT')) {
17
+ console.log('\n\n👋 Operation cancelled. Goodbye!\n');
18
+ process.exit(0);
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ /**
24
+ * Setup global SIGINT handler for graceful exits
25
+ */
26
+ export function setupGlobalInterruptHandler() {
27
+ process.on('SIGINT', () => {
28
+ console.log('\n\n👋 Operation cancelled. Goodbye!\n');
29
+ process.exit(0);
30
+ });
31
+ }