lua-cli 1.2.1 → 1.3.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.
@@ -0,0 +1,822 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { gzipSync, gunzipSync } from "zlib";
4
+ import { Buffer } from "buffer";
5
+ import { withErrorHandling, writeProgress, writeSuccess } from "../utils/cli.js";
6
+ // Compression utilities
7
+ function compressCode(code) {
8
+ const compressed = gzipSync(code);
9
+ return compressed.toString('base64');
10
+ }
11
+ function decompressCode(compressedCode) {
12
+ const buffer = Buffer.from(compressedCode, 'base64');
13
+ return gunzipSync(buffer).toString('utf8');
14
+ }
15
+ export async function compileCommand() {
16
+ return withErrorHandling(async () => {
17
+ writeProgress("🔨 Compiling Lua skill...");
18
+ // Read package.json to get version and name
19
+ const packageJsonPath = path.join(process.cwd(), "package.json");
20
+ if (!fs.existsSync(packageJsonPath)) {
21
+ throw new Error("package.json not found in current directory");
22
+ }
23
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
24
+ const packageVersion = packageJson.version || "1.0.0";
25
+ const skillsName = packageJson.name || "lua-skill";
26
+ // Read index.ts file
27
+ const indexPath = path.join(process.cwd(), "index.ts");
28
+ if (!fs.existsSync(indexPath)) {
29
+ throw new Error("index.ts not found in current directory");
30
+ }
31
+ const indexContent = fs.readFileSync(indexPath, "utf8");
32
+ // Extract skill information
33
+ const skillInfo = await extractSkillInfo(indexContent);
34
+ // Extract skill metadata from LuaSkill constructor and TOML file
35
+ const skillMetadata = await extractSkillMetadata(indexContent);
36
+ // Use version from TOML file, fallback to package.json version
37
+ const version = skillMetadata.version || packageVersion || "1.0.0";
38
+ // Create deployment data with compressed execute code
39
+ const deployData = {
40
+ version,
41
+ skillsName,
42
+ skillId: skillMetadata.skillId,
43
+ description: skillMetadata.description,
44
+ context: skillMetadata.context,
45
+ tools: skillInfo.map(tool => ({
46
+ ...tool,
47
+ execute: compressCode(tool.execute)
48
+ }))
49
+ };
50
+ // Create .lua directory
51
+ const luaDir = path.join(process.cwd(), ".lua");
52
+ if (!fs.existsSync(luaDir)) {
53
+ fs.mkdirSync(luaDir, { recursive: true });
54
+ }
55
+ // Write JSON output to .lua directory
56
+ const jsonOutputPath = path.join(luaDir, "deploy.json");
57
+ fs.writeFileSync(jsonOutputPath, JSON.stringify(deployData, null, 2));
58
+ // Write individual tool files to .lua directory
59
+ for (const tool of skillInfo) {
60
+ const toolFilePath = path.join(luaDir, `${tool.name}.js`);
61
+ fs.writeFileSync(toolFilePath, tool.execute);
62
+ }
63
+ writeSuccess("✅ Skill compiled successfully");
64
+ }, "compilation");
65
+ }
66
+ async function extractSkillInfo(indexContent) {
67
+ const tools = [];
68
+ // Find inline addTool calls: skill.addTool({...})
69
+ const inlineAddToolRegex = /skill\.addTool\(\s*\{([\s\S]*?)\}\s*\)/g;
70
+ let match;
71
+ while ((match = inlineAddToolRegex.exec(indexContent)) !== null) {
72
+ const toolContent = match[1];
73
+ // Extract tool properties
74
+ const nameMatch = toolContent.match(/name:\s*["']([^"']+)["']/);
75
+ const descriptionMatch = toolContent.match(/description:\s*["']([^"']+)["']/);
76
+ const inputSchemaMatch = toolContent.match(/inputSchema:\s*(\w+)/);
77
+ const outputSchemaMatch = toolContent.match(/outputSchema:\s*(\w+)/);
78
+ const executeMatch = toolContent.match(/execute:\s*async\s*\([^)]*\)\s*=>\s*\{([\s\S]*?)\}/);
79
+ if (nameMatch && descriptionMatch && inputSchemaMatch && outputSchemaMatch && executeMatch) {
80
+ const toolName = nameMatch[1];
81
+ const toolDescription = descriptionMatch[1];
82
+ const inputSchemaVar = inputSchemaMatch[1];
83
+ const outputSchemaVar = outputSchemaMatch[1];
84
+ const executeBody = executeMatch[1];
85
+ // Convert schemas to JSON Schema format
86
+ const inputSchema = convertSchemaToJSON(inputSchemaVar, indexContent);
87
+ const outputSchema = convertSchemaToJSON(outputSchemaVar, indexContent);
88
+ // Create self-contained execute function
89
+ const selfContainedExecute = await createSelfContainedExecute(executeBody, indexContent);
90
+ tools.push({
91
+ name: toolName,
92
+ description: toolDescription,
93
+ inputSchema,
94
+ outputSchema,
95
+ execute: selfContainedExecute
96
+ });
97
+ }
98
+ }
99
+ // Find class-based addTool calls: skill.addTool(new SomeTool())
100
+ const classAddToolRegex = /skill\.addTool\(\s*new\s+(\w+)\(\)\s*\)/g;
101
+ let classMatch;
102
+ while ((classMatch = classAddToolRegex.exec(indexContent)) !== null) {
103
+ const className = classMatch[1];
104
+ // Find the tool class definition
105
+ const toolInfo = await extractToolFromClass(className, indexContent);
106
+ if (toolInfo) {
107
+ tools.push(toolInfo);
108
+ }
109
+ }
110
+ return tools;
111
+ }
112
+ function convertSchemaToJSON(schemaVar, indexContent) {
113
+ // Find the schema definition
114
+ const schemaRegex = new RegExp(`const\\s+${schemaVar}\\s*=\\s*z\\.object\\(\\{([\\s\\S]*?)\\}\\\);`, 'g');
115
+ const match = schemaRegex.exec(indexContent);
116
+ if (match) {
117
+ const schemaContent = match[1];
118
+ // Convert Zod schema to JSON Schema format
119
+ return {
120
+ type: "object",
121
+ properties: parseZodProperties(schemaContent),
122
+ required: extractRequiredFields(schemaContent)
123
+ };
124
+ }
125
+ // If no match found, return empty schema
126
+ return { type: "object", properties: {} };
127
+ }
128
+ function parseZodProperties(schemaContent) {
129
+ const properties = {};
130
+ // Simple regex to find z.string(), z.number(), etc.
131
+ const fieldRegex = /(\w+):\s*z\.(\w+)\(\)/g;
132
+ let match;
133
+ while ((match = fieldRegex.exec(schemaContent)) !== null) {
134
+ const fieldName = match[1];
135
+ const fieldType = match[2];
136
+ switch (fieldType) {
137
+ case 'string':
138
+ properties[fieldName] = { type: 'string' };
139
+ break;
140
+ case 'number':
141
+ properties[fieldName] = { type: 'number' };
142
+ break;
143
+ case 'boolean':
144
+ properties[fieldName] = { type: 'boolean' };
145
+ break;
146
+ default:
147
+ properties[fieldName] = { type: 'string' };
148
+ }
149
+ }
150
+ return properties;
151
+ }
152
+ function extractRequiredFields(schemaContent) {
153
+ const required = [];
154
+ const fieldRegex = /(\w+):\s*z\.(\w+)\(\)/g;
155
+ let match;
156
+ while ((match = fieldRegex.exec(schemaContent)) !== null) {
157
+ required.push(match[1]);
158
+ }
159
+ return required;
160
+ }
161
+ async function createSelfContainedExecute(executeBody, indexContent) {
162
+ const dependencies = [];
163
+ const bundledPackages = new Set();
164
+ // 1. Parse external package imports and bundle their code
165
+ const allImportRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
166
+ let importMatch;
167
+ while ((importMatch = allImportRegex.exec(indexContent)) !== null) {
168
+ const namedImports = importMatch[1]; // Named imports like { z }
169
+ const defaultImport = importMatch[2]; // Default import like axios
170
+ const packagePath = importMatch[3];
171
+ // Skip local imports (relative paths)
172
+ if (packagePath.startsWith('./') || packagePath.startsWith('../')) {
173
+ continue;
174
+ }
175
+ // Skip lua-cli imports (these are handled separately)
176
+ if (packagePath.startsWith('lua-cli')) {
177
+ continue;
178
+ }
179
+ // Skip zod - assume it's always available on target machine
180
+ if (packagePath === 'zod') {
181
+ // Add require statement for zod instead of bundling
182
+ if (namedImports) {
183
+ const importsList = namedImports.split(',').map(imp => imp.trim());
184
+ const usedImports = importsList.filter(imp => executeBody.includes(imp) || indexContent.includes(`${imp}.`));
185
+ if (usedImports.length > 0) {
186
+ const requireStatement = usedImports.length === 1
187
+ ? `const { ${usedImports[0]} } = require('zod');`
188
+ : `const { ${usedImports.join(', ')} } = require('zod');`;
189
+ bundledPackages.add(requireStatement);
190
+ }
191
+ }
192
+ else if (defaultImport) {
193
+ bundledPackages.add(`const ${defaultImport} = require('zod');`);
194
+ }
195
+ continue;
196
+ }
197
+ // Bundle other external packages
198
+ if (namedImports || defaultImport) {
199
+ const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
200
+ if (packageCode) {
201
+ bundledPackages.add(packageCode);
202
+ }
203
+ }
204
+ }
205
+ // 2. Extract class definitions with proper brace matching
206
+ const classRegex = /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g;
207
+ let classMatch;
208
+ while ((classMatch = classRegex.exec(indexContent)) !== null) {
209
+ const className = classMatch[1];
210
+ const classStart = classMatch.index;
211
+ // Find the matching closing brace
212
+ let braceCount = 0;
213
+ let classEnd = classStart;
214
+ let found = false;
215
+ for (let i = classStart; i < indexContent.length; i++) {
216
+ if (indexContent[i] === '{') {
217
+ braceCount++;
218
+ }
219
+ else if (indexContent[i] === '}') {
220
+ braceCount--;
221
+ if (braceCount === 0) {
222
+ classEnd = i;
223
+ found = true;
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ if (found) {
229
+ const fullClass = indexContent.substring(classStart, classEnd + 1);
230
+ // Check if this class is used in the execute function
231
+ let isUsed = false;
232
+ // Direct usage in execute body
233
+ if (executeBody.includes(`new ${className}`) ||
234
+ executeBody.includes(`${className}.`) ||
235
+ executeBody.includes(`${className}(`)) {
236
+ isUsed = true;
237
+ }
238
+ // Check if any variable that uses this class is referenced in execute body
239
+ const variableRegex = new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${className}\\s*\\([^)]*\\);`, 'g');
240
+ let varMatch;
241
+ while ((varMatch = variableRegex.exec(indexContent)) !== null) {
242
+ const varName = varMatch[1];
243
+ if (executeBody.includes(varName)) {
244
+ isUsed = true;
245
+ break;
246
+ }
247
+ }
248
+ if (isUsed) {
249
+ dependencies.push(fullClass);
250
+ }
251
+ }
252
+ }
253
+ // 3. Extract function definitions
254
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\}/g;
255
+ let functionMatch;
256
+ while ((functionMatch = functionRegex.exec(indexContent)) !== null) {
257
+ const functionName = functionMatch[1];
258
+ const functionBody = functionMatch[2];
259
+ if (executeBody.includes(functionName)) {
260
+ dependencies.push(`function ${functionName}() {\n${functionBody}\n}`);
261
+ }
262
+ }
263
+ // 4. Extract const/let/var declarations (avoid duplicates)
264
+ const varRegex = /(?:const|let|var)\s+(\w+)\s*=\s*([^;]+);/g;
265
+ const declaredVars = new Set();
266
+ let varMatch;
267
+ while ((varMatch = varRegex.exec(indexContent)) !== null) {
268
+ const varName = varMatch[1];
269
+ const varValue = varMatch[2];
270
+ // Skip if it's a class instantiation (we'll handle that separately)
271
+ if (varValue.includes('new ') && varValue.includes('()')) {
272
+ continue;
273
+ }
274
+ // Skip if already declared
275
+ if (declaredVars.has(varName)) {
276
+ continue;
277
+ }
278
+ if (executeBody.includes(varName)) {
279
+ declaredVars.add(varName);
280
+ dependencies.push(`const ${varName} = ${varValue};`);
281
+ }
282
+ }
283
+ // 5. Extract class instantiations (avoid duplicates)
284
+ const instantiationRegex = /(?:const|let|var)\s+(\w+)\s*=\s*new\s+(\w+)\([^)]*\);/g;
285
+ let instantiationMatch;
286
+ while ((instantiationMatch = instantiationRegex.exec(indexContent)) !== null) {
287
+ const instanceName = instantiationMatch[1];
288
+ const className = instantiationMatch[2];
289
+ // Skip if already declared
290
+ if (declaredVars.has(instanceName)) {
291
+ continue;
292
+ }
293
+ if (executeBody.includes(instanceName)) {
294
+ declaredVars.add(instanceName);
295
+ dependencies.push(`const ${instanceName} = new ${className}();`);
296
+ }
297
+ }
298
+ // 6. Create the self-contained execute function
299
+ const allDependencies = [...Array.from(bundledPackages), ...dependencies];
300
+ const dependencyCode = allDependencies.join('\n');
301
+ // Strip TypeScript type annotations for JavaScript compatibility (only for local code)
302
+ const cleanDependencyCode = allDependencies.map(dep => {
303
+ // Only strip TypeScript from local dependencies, not bundled packages
304
+ if (dep.includes('require(') || dep.includes('import ')) {
305
+ return dep; // Skip bundled packages
306
+ }
307
+ return dep
308
+ .replace(/:\s*string/g, '') // Remove : string
309
+ .replace(/:\s*number/g, '') // Remove : number
310
+ .replace(/:\s*boolean/g, '') // Remove : boolean
311
+ .replace(/:\s*any/g, '') // Remove : any
312
+ .replace(/:\s*void/g, '') // Remove : void
313
+ .replace(/:\s*object/g, '') // Remove : object
314
+ .replace(/:\s*Array<[^>]+>/g, '') // Remove : Array<Type>
315
+ .replace(/:\s*Promise<[^>]+>/g, '') // Remove : Promise<Type>
316
+ .replace(/:\s*Record<[^>]+>/g, ''); // Remove : Record<Type>
317
+ }).join('\n');
318
+ const cleanExecuteBody = executeBody
319
+ .replace(/:\s*string/g, '') // Remove : string
320
+ .replace(/:\s*number/g, '') // Remove : number
321
+ .replace(/:\s*boolean/g, '') // Remove : boolean
322
+ .replace(/:\s*any/g, '') // Remove : any
323
+ .replace(/:\s*void/g, '') // Remove : void
324
+ .replace(/:\s*object/g, '') // Remove : object
325
+ .replace(/:\s*Array<[^>]+>/g, '') // Remove : Array<Type>
326
+ .replace(/:\s*Promise<[^>]+>/g, '') // Remove : Promise<Type>
327
+ .replace(/:\s*Record<[^>]+>/g, ''); // Remove : Record<Type>
328
+ const selfContainedExecute = `async (input) => {
329
+ ${cleanDependencyCode ? ` ${cleanDependencyCode.split('\n').join('\n ')}\n` : ''} ${cleanExecuteBody.trim()}
330
+ }`;
331
+ return selfContainedExecute;
332
+ }
333
+ async function bundlePackageCode(packagePath, namedImports, defaultImport) {
334
+ try {
335
+ const { build } = await import('esbuild');
336
+ // Create a temporary entry file for esbuild in .lua directory
337
+ const luaDir = path.join(process.cwd(), '.lua');
338
+ if (!fs.existsSync(luaDir)) {
339
+ fs.mkdirSync(luaDir, { recursive: true });
340
+ }
341
+ const entryFile = path.join(luaDir, `${packagePath}-entry.cjs`);
342
+ const outputFile = path.join(luaDir, `${packagePath}-bundle.cjs`);
343
+ // Create entry file based on import type
344
+ let entryContent = '';
345
+ if (defaultImport) {
346
+ // For default imports like `import axios from 'axios'`
347
+ entryContent = `import ${defaultImport} from '${packagePath}';\nmodule.exports = ${defaultImport};`;
348
+ }
349
+ else if (namedImports) {
350
+ // For named imports like `import { z } from 'zod'`
351
+ const importsList = namedImports.split(',').map(imp => imp.trim());
352
+ entryContent = `import { ${importsList.join(', ')} } from '${packagePath}';\nmodule.exports = { ${importsList.join(', ')} };`;
353
+ }
354
+ else {
355
+ // Fallback - import everything
356
+ entryContent = `import * as ${packagePath.replace(/[^a-zA-Z0-9]/g, '_')} from '${packagePath}';\nmodule.exports = ${packagePath.replace(/[^a-zA-Z0-9]/g, '_')};`;
357
+ }
358
+ // Write entry file
359
+ fs.writeFileSync(entryFile, entryContent);
360
+ // Bundle with esbuild
361
+ const result = await build({
362
+ entryPoints: [entryFile],
363
+ bundle: true,
364
+ format: 'cjs', // CommonJS format
365
+ platform: 'node',
366
+ target: 'node16',
367
+ outfile: outputFile,
368
+ external: [], // Bundle everything
369
+ minify: false, // Keep readable for debugging
370
+ sourcemap: false,
371
+ write: true,
372
+ resolveExtensions: ['.js', '.ts', '.json'],
373
+ mainFields: ['main', 'module', 'browser'],
374
+ conditions: ['node'],
375
+ nodePaths: [
376
+ path.join(process.cwd(), 'node_modules'),
377
+ path.join(process.cwd(), '..', 'node_modules'),
378
+ path.join(process.cwd(), '..', '..', 'node_modules')
379
+ ],
380
+ absWorkingDir: process.cwd(),
381
+ });
382
+ if (result.errors.length > 0) {
383
+ console.warn(`Warning: esbuild errors for package ${packagePath}:`, result.errors);
384
+ return null;
385
+ }
386
+ // Read the bundled output
387
+ if (!fs.existsSync(outputFile)) {
388
+ console.warn(`Warning: Bundle output not found for package ${packagePath}`);
389
+ return null;
390
+ }
391
+ const bundledContent = fs.readFileSync(outputFile, 'utf8');
392
+ // Clean up temporary files
393
+ try {
394
+ fs.unlinkSync(entryFile);
395
+ fs.unlinkSync(outputFile);
396
+ }
397
+ catch (cleanupError) {
398
+ // Ignore cleanup errors
399
+ }
400
+ // Create the final bundled code
401
+ let finalCode = '';
402
+ if (defaultImport) {
403
+ finalCode = `const ${defaultImport} = (function() {\n${bundledContent}\n return module.exports;\n})();\n`;
404
+ }
405
+ else if (namedImports) {
406
+ const importsList = namedImports.split(',').map(imp => imp.trim());
407
+ finalCode = `(function() {\n${bundledContent}\n})();\n`;
408
+ finalCode += `const { ${importsList.join(', ')} } = module.exports;\n`;
409
+ }
410
+ else {
411
+ finalCode = `(function() {\n${bundledContent}\n})();\n`;
412
+ }
413
+ return finalCode;
414
+ }
415
+ catch (error) {
416
+ console.warn(`Warning: Could not bundle package ${packagePath} with esbuild:`, error);
417
+ // For test environments or when esbuild fails, provide a fallback
418
+ if (packagePath === 'axios') {
419
+ return createWorkingAxiosImplementation();
420
+ }
421
+ return null;
422
+ }
423
+ }
424
+ function createWorkingAxiosImplementation() {
425
+ return `
426
+ // Working axios implementation using native fetch (for test environments)
427
+ const axios = {
428
+ get: async (url, config = {}) => {
429
+ const searchParams = new URLSearchParams(config.params || {});
430
+ const fullUrl = searchParams.toString() ? \`\${url}?\${searchParams}\` : url;
431
+
432
+ const response = await fetch(fullUrl, {
433
+ method: 'GET',
434
+ headers: {
435
+ 'Content-Type': 'application/json',
436
+ ...config.headers
437
+ }
438
+ });
439
+
440
+ if (!response.ok) {
441
+ const error = new Error(\`Request failed with status \${response.status}\`);
442
+ error.response = { status: response.status, statusText: response.statusText };
443
+ throw error;
444
+ }
445
+
446
+ const data = await response.json();
447
+ return {
448
+ data,
449
+ status: response.status,
450
+ statusText: response.statusText,
451
+ headers: response.headers,
452
+ config: config
453
+ };
454
+ },
455
+
456
+ post: async (url, data, config = {}) => {
457
+ const response = await fetch(url, {
458
+ method: 'POST',
459
+ headers: {
460
+ 'Content-Type': 'application/json',
461
+ ...config.headers
462
+ },
463
+ body: JSON.stringify(data)
464
+ });
465
+
466
+ if (!response.ok) {
467
+ const error = new Error(\`Request failed with status \${response.status}\`);
468
+ error.response = { status: response.status, statusText: response.statusText };
469
+ throw error;
470
+ }
471
+
472
+ const responseData = await response.json();
473
+ return {
474
+ data: responseData,
475
+ status: response.status,
476
+ statusText: response.statusText,
477
+ headers: response.headers,
478
+ config: config
479
+ };
480
+ },
481
+
482
+ put: async (url, data, config = {}) => {
483
+ const response = await fetch(url, {
484
+ method: 'PUT',
485
+ headers: {
486
+ 'Content-Type': 'application/json',
487
+ ...config.headers
488
+ },
489
+ body: JSON.stringify(data)
490
+ });
491
+
492
+ if (!response.ok) {
493
+ const error = new Error(\`Request failed with status \${response.status}\`);
494
+ error.response = { status: response.status, statusText: response.statusText };
495
+ throw error;
496
+ }
497
+
498
+ const responseData = await response.json();
499
+ return {
500
+ data: responseData,
501
+ status: response.status,
502
+ statusText: response.statusText,
503
+ headers: response.headers,
504
+ config: config
505
+ };
506
+ },
507
+
508
+ delete: async (url, config = {}) => {
509
+ const response = await fetch(url, {
510
+ method: 'DELETE',
511
+ headers: {
512
+ 'Content-Type': 'application/json',
513
+ ...config.headers
514
+ }
515
+ });
516
+
517
+ if (!response.ok) {
518
+ const error = new Error(\`Request failed with status \${response.status}\`);
519
+ error.response = { status: response.status, statusText: response.statusText };
520
+ throw error;
521
+ }
522
+
523
+ const responseData = await response.json();
524
+ return {
525
+ data: responseData,
526
+ status: response.status,
527
+ statusText: response.statusText,
528
+ headers: response.headers,
529
+ config: config
530
+ };
531
+ },
532
+
533
+ patch: async (url, data, config = {}) => {
534
+ const response = await fetch(url, {
535
+ method: 'PATCH',
536
+ headers: {
537
+ 'Content-Type': 'application/json',
538
+ ...config.headers
539
+ },
540
+ body: JSON.stringify(data)
541
+ });
542
+
543
+ if (!response.ok) {
544
+ const error = new Error(\`Request failed with status \${response.status}\`);
545
+ error.response = { status: response.status, statusText: response.statusText };
546
+ throw error;
547
+ }
548
+
549
+ const responseData = await response.json();
550
+ return {
551
+ data: responseData,
552
+ status: response.status,
553
+ statusText: response.statusText,
554
+ headers: response.headers,
555
+ config: config
556
+ };
557
+ }
558
+ };
559
+ `;
560
+ }
561
+ async function extractToolFromClass(className, indexContent) {
562
+ // Find the import statement for this class
563
+ const importRegex = new RegExp(`import\\s+${className}\\s+from\\s+["']([^"']+)["']`, 'g');
564
+ const importMatch = importRegex.exec(indexContent);
565
+ if (!importMatch) {
566
+ console.warn(`Warning: Could not find import for class ${className}`);
567
+ return null;
568
+ }
569
+ const importPath = importMatch[1];
570
+ // Read the tool file
571
+ const toolFilePath = path.join(process.cwd(), importPath.replace('./', '') + '.ts');
572
+ if (!fs.existsSync(toolFilePath)) {
573
+ console.warn(`Warning: Tool file not found: ${toolFilePath}`);
574
+ return null;
575
+ }
576
+ const toolContent = fs.readFileSync(toolFilePath, 'utf8');
577
+ // Extract tool properties from the class
578
+ const nameMatch = toolContent.match(/this\.name\s*=\s*["']([^"']+)["']/);
579
+ const descriptionMatch = toolContent.match(/this\.description\s*=\s*["']([^"']+)["']/);
580
+ if (!nameMatch || !descriptionMatch) {
581
+ console.warn(`Warning: Could not extract name or description from ${className}`);
582
+ return null;
583
+ }
584
+ const toolName = nameMatch[1];
585
+ const toolDescription = descriptionMatch[1];
586
+ // Extract schemas
587
+ const inputSchemaMatch = toolContent.match(/const\s+(\w+)\s*=\s*z\.object\(/);
588
+ const outputSchemaMatch = toolContent.match(/const\s+(\w+)\s*=\s*z\.object\(/);
589
+ if (!inputSchemaMatch) {
590
+ console.warn(`Warning: Could not find input schema in ${className}`);
591
+ return null;
592
+ }
593
+ // Convert schemas to JSON Schema format
594
+ const inputSchema = convertSchemaToJSON(inputSchemaMatch[1], toolContent);
595
+ const outputSchema = outputSchemaMatch ? convertSchemaToJSON(outputSchemaMatch[1], toolContent) : { type: "object" };
596
+ // Extract execute method
597
+ const executeMatch = toolContent.match(/async\s+execute\s*\([^)]*\)\s*\{([\s\S]*?)\}/);
598
+ if (!executeMatch) {
599
+ console.warn(`Warning: Could not find execute method in ${className}`);
600
+ return null;
601
+ }
602
+ const executeBody = executeMatch[1];
603
+ // For class-based tools, we need to create a self-contained function that includes:
604
+ // 1. The service classes
605
+ // 2. Class instantiation
606
+ // 3. The execute logic
607
+ const selfContainedExecute = await createClassBasedExecute(executeBody, toolContent, className);
608
+ return {
609
+ name: toolName,
610
+ description: toolDescription,
611
+ inputSchema,
612
+ outputSchema,
613
+ execute: selfContainedExecute
614
+ };
615
+ }
616
+ async function createClassBasedExecute(executeBody, toolContent, className) {
617
+ const dependencies = [];
618
+ const bundledPackages = new Set();
619
+ // 1. Parse imports from the tool file
620
+ const importRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
621
+ let importMatch;
622
+ while ((importMatch = importRegex.exec(toolContent)) !== null) {
623
+ const namedImports = importMatch[1];
624
+ const defaultImport = importMatch[2];
625
+ const packagePath = importMatch[3];
626
+ // Skip lua-cli imports
627
+ if (packagePath.startsWith('lua-cli')) {
628
+ continue;
629
+ }
630
+ // Handle zod
631
+ if (packagePath === 'zod') {
632
+ if (namedImports) {
633
+ const importsList = namedImports.split(',').map(imp => imp.trim());
634
+ const usedImports = importsList.filter(imp => executeBody.includes(imp) || toolContent.includes(`${imp}.`));
635
+ if (usedImports.length > 0) {
636
+ const requireStatement = usedImports.length === 1
637
+ ? `const { ${usedImports[0]} } = require('zod');`
638
+ : `const { ${usedImports.join(', ')} } = require('zod');`;
639
+ bundledPackages.add(requireStatement);
640
+ }
641
+ }
642
+ else if (defaultImport) {
643
+ bundledPackages.add(`const ${defaultImport} = require('zod');`);
644
+ }
645
+ continue;
646
+ }
647
+ // Handle axios - bundle it properly
648
+ if (packagePath === 'axios') {
649
+ const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
650
+ if (packageCode) {
651
+ bundledPackages.add(packageCode);
652
+ }
653
+ continue;
654
+ }
655
+ // Handle local service imports
656
+ if (packagePath.startsWith('./') || packagePath.startsWith('../')) {
657
+ // The tool files are in tools/ subdirectory, so we need to resolve from there
658
+ const toolDir = path.join(process.cwd(), 'tools');
659
+ // Resolve the service file path correctly
660
+ // If the import is ../services/ApiService, resolve it relative to the tools directory
661
+ const serviceFilePath = path.resolve(process.cwd(), 'tools', packagePath + '.ts');
662
+ if (fs.existsSync(serviceFilePath)) {
663
+ const serviceContent = fs.readFileSync(serviceFilePath, 'utf8');
664
+ // Process all imports in the service file
665
+ const serviceImportRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
666
+ let serviceImportMatch;
667
+ while ((serviceImportMatch = serviceImportRegex.exec(serviceContent)) !== null) {
668
+ const namedImports = serviceImportMatch[1];
669
+ const defaultImport = serviceImportMatch[2];
670
+ const packagePath = serviceImportMatch[3];
671
+ // Skip lua-cli imports
672
+ if (packagePath.startsWith('lua-cli')) {
673
+ continue;
674
+ }
675
+ // Handle zod
676
+ if (packagePath === 'zod') {
677
+ if (namedImports) {
678
+ const importsList = namedImports.split(',').map(imp => imp.trim());
679
+ const usedImports = importsList.filter(imp => serviceContent.includes(`${imp}.`));
680
+ if (usedImports.length > 0) {
681
+ const requireStatement = usedImports.length === 1
682
+ ? `const { ${usedImports[0]} } = require('zod');`
683
+ : `const { ${usedImports.join(', ')} } = require('zod');`;
684
+ bundledPackages.add(requireStatement);
685
+ }
686
+ }
687
+ else if (defaultImport) {
688
+ bundledPackages.add(`const ${defaultImport} = require('zod');`);
689
+ }
690
+ continue;
691
+ }
692
+ // Handle axios - bundle it properly
693
+ if (packagePath === 'axios') {
694
+ const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
695
+ if (packageCode) {
696
+ bundledPackages.add(packageCode);
697
+ }
698
+ continue;
699
+ }
700
+ // Bundle other external packages
701
+ if (namedImports || defaultImport) {
702
+ const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
703
+ if (packageCode) {
704
+ bundledPackages.add(packageCode);
705
+ }
706
+ }
707
+ }
708
+ // Extract the service class with proper brace matching
709
+ const classRegex = /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g;
710
+ let classMatch = classRegex.exec(serviceContent);
711
+ if (classMatch) {
712
+ const serviceClassName = classMatch[1];
713
+ const startIndex = classMatch.index + classMatch[0].length - 1; // Position of opening brace
714
+ // Find matching closing brace
715
+ let braceCount = 1;
716
+ let endIndex = startIndex + 1;
717
+ while (endIndex < serviceContent.length && braceCount > 0) {
718
+ if (serviceContent[endIndex] === '{')
719
+ braceCount++;
720
+ else if (serviceContent[endIndex] === '}')
721
+ braceCount--;
722
+ endIndex++;
723
+ }
724
+ if (braceCount === 0) {
725
+ const serviceClassBody = serviceContent.substring(startIndex + 1, endIndex - 1);
726
+ // Clean up the class body (remove TypeScript types)
727
+ const cleanClassBody = serviceClassBody
728
+ .replace(/:\s*string/g, '')
729
+ .replace(/:\s*number/g, '')
730
+ .replace(/:\s*boolean/g, '')
731
+ .replace(/:\s*any/g, '')
732
+ .replace(/:\s*void/g, '')
733
+ .replace(/:\s*Promise<[^>]+>/g, '')
734
+ .replace(/:\s*Record<[^>]+>/g, '');
735
+ // Create the service class
736
+ const serviceClass = `class ${serviceClassName} {\n${cleanClassBody}\n}`;
737
+ dependencies.push(serviceClass);
738
+ }
739
+ }
740
+ }
741
+ continue;
742
+ }
743
+ // Bundle other external packages
744
+ if (namedImports || defaultImport) {
745
+ const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
746
+ if (packageCode) {
747
+ bundledPackages.add(packageCode);
748
+ }
749
+ }
750
+ }
751
+ // 2. Extract class instantiation from constructor
752
+ const constructorMatch = toolContent.match(/constructor\s*\([^)]*\)\s*\{([\s\S]*?)\}/);
753
+ if (constructorMatch) {
754
+ const constructorBody = constructorMatch[1];
755
+ // Extract service instantiation
756
+ const serviceInstantiationMatch = constructorBody.match(/this\.(\w+)\s*=\s*new\s+(\w+)\([^)]*\);/);
757
+ if (serviceInstantiationMatch) {
758
+ const serviceProperty = serviceInstantiationMatch[1];
759
+ const serviceClass = serviceInstantiationMatch[2];
760
+ dependencies.push(`const ${serviceProperty} = new ${serviceClass}();`);
761
+ }
762
+ }
763
+ // 3. Create the self-contained execute function
764
+ const allDependencies = [...Array.from(bundledPackages), ...dependencies];
765
+ const dependencyCode = allDependencies.join('\n');
766
+ // Clean the execute body (remove TypeScript types)
767
+ const cleanExecuteBody = executeBody
768
+ .replace(/:\s*string/g, '')
769
+ .replace(/:\s*number/g, '')
770
+ .replace(/:\s*boolean/g, '')
771
+ .replace(/:\s*any/g, '')
772
+ .replace(/:\s*void/g, '')
773
+ .replace(/:\s*Promise<[^>]+>/g, '')
774
+ .replace(/:\s*Record<[^>]+>/g, '');
775
+ // Replace this.serviceProperty with the instantiated service
776
+ const finalExecuteBody = cleanExecuteBody.replace(/this\.(\w+)/g, '$1');
777
+ return `async (input) => {
778
+ ${dependencyCode ? dependencyCode + '\n' : ''}${finalExecuteBody}
779
+ }`;
780
+ }
781
+ async function extractSkillMetadata(indexContent) {
782
+ // Extract skillId and version from TOML file
783
+ let skillId = '';
784
+ let version = '';
785
+ const tomlPath = path.join(process.cwd(), 'lua.skill.toml');
786
+ if (fs.existsSync(tomlPath)) {
787
+ const tomlContent = fs.readFileSync(tomlPath, 'utf8');
788
+ const skillIdMatch = tomlContent.match(/skillId\s*=\s*["']([^"']+)["']/);
789
+ if (skillIdMatch) {
790
+ skillId = skillIdMatch[1];
791
+ }
792
+ const versionMatch = tomlContent.match(/version\s*=\s*["']([^"']+)["']/);
793
+ if (versionMatch) {
794
+ version = versionMatch[1];
795
+ }
796
+ }
797
+ // Extract description and context from LuaSkill constructor
798
+ let description = '';
799
+ let context = '';
800
+ // Look for LuaSkill constructor with object configuration
801
+ const luaSkillRegex = /new\s+LuaSkill\s*\(\s*\{([\s\S]*?)\}\s*\)/;
802
+ const match = luaSkillRegex.exec(indexContent);
803
+ if (match) {
804
+ const configContent = match[1];
805
+ // Extract description
806
+ const descriptionMatch = configContent.match(/description:\s*["']([^"']+)["']/);
807
+ if (descriptionMatch) {
808
+ description = descriptionMatch[1];
809
+ }
810
+ // Extract context
811
+ const contextMatch = configContent.match(/context:\s*["']([^"']+)["']/);
812
+ if (contextMatch) {
813
+ context = contextMatch[1];
814
+ }
815
+ }
816
+ return {
817
+ skillId,
818
+ version,
819
+ description,
820
+ context
821
+ };
822
+ }