lua-cli 1.3.2-alpha.2 → 2.0.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.
@@ -1,1066 +1,725 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { gzipSync, gunzipSync } from "zlib";
4
- import { Buffer } from "buffer";
3
+ import { gzipSync } from "zlib";
5
4
  import { withErrorHandling, writeProgress, writeSuccess } from "../utils/cli.js";
6
5
  import { readSkillConfig } from '../utils/files.js';
7
- import * as ts from "typescript";
6
+ import { loadApiKey } from '../services/auth.js';
7
+ import { ApiService } from '../services/api.js';
8
+ import { Project, Node } from "ts-morph";
9
+ import { build } from "esbuild";
10
+ import yaml from "js-yaml";
8
11
  // Compression utilities
9
12
  function compressCode(code) {
10
13
  const compressed = gzipSync(code);
11
14
  return compressed.toString('base64');
12
15
  }
13
- function decompressCode(compressedCode) {
14
- const buffer = Buffer.from(compressedCode, 'base64');
15
- return gunzipSync(buffer).toString('utf8');
16
- }
17
16
  export async function compileCommand() {
18
17
  return withErrorHandling(async () => {
19
18
  writeProgress("🔨 Compiling Lua skill...");
20
- // Clean up old .lua directory
21
- const oldLuaDir = path.join(process.cwd(), ".lua");
22
- if (fs.existsSync(oldLuaDir)) {
23
- fs.rmSync(oldLuaDir, { recursive: true, force: true });
24
- }
25
- // Read package.json to get version and name
26
- const packageJsonPath = path.join(process.cwd(), "package.json");
27
- if (!fs.existsSync(packageJsonPath)) {
28
- throw new Error("package.json not found in current directory");
29
- }
30
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
31
- const packageVersion = packageJson.version || "1.0.0";
32
- // Get skill name from config file, fallback to package.json name
33
- const config = readSkillConfig();
34
- const skillName = config?.skill?.name || packageJson.name || "lua-skill";
35
- // Read index.ts file (check both root and src directory)
36
- let indexPath = path.join(process.cwd(), "index.ts");
37
- if (!fs.existsSync(indexPath)) {
38
- indexPath = path.join(process.cwd(), "src", "index.ts");
39
- if (!fs.existsSync(indexPath)) {
40
- throw new Error("index.ts not found in current directory or src/ directory");
41
- }
42
- }
43
- const indexContent = fs.readFileSync(indexPath, "utf8");
44
- // Extract skill information
45
- const skillInfo = await extractSkillInfo(indexContent, indexPath);
46
- // Extract skill metadata from LuaSkill constructor and config file
47
- const skillMetadata = await extractSkillMetadata(indexContent);
48
- // Use version from config file, fallback to package.json version
49
- const version = skillMetadata.version || packageVersion || "1.0.0";
50
- // Create deployment data with compressed execute code
51
- const deployData = {
52
- version,
53
- name: skillName,
54
- skillsName: skillName,
55
- skillId: skillMetadata.skillId,
56
- description: skillMetadata.description,
57
- context: skillMetadata.context,
58
- tools: skillInfo.map(tool => ({
59
- ...tool,
60
- execute: compressCode(tool.execute)
61
- }))
62
- };
63
- // Create .lua directory
19
+ // Clean up old directories
20
+ const distDir = path.join(process.cwd(), "dist");
64
21
  const luaDir = path.join(process.cwd(), ".lua");
65
- if (!fs.existsSync(luaDir)) {
66
- fs.mkdirSync(luaDir, { recursive: true });
67
- }
68
- // Write JSON output to .lua directory
69
- const jsonOutputPath = path.join(luaDir, "deploy.json");
70
- fs.writeFileSync(jsonOutputPath, JSON.stringify(deployData, null, 2));
71
- // Write individual tool files to .lua directory
72
- for (const tool of skillInfo) {
73
- const toolFilePath = path.join(luaDir, `${tool.name}.js`);
74
- fs.writeFileSync(toolFilePath, tool.execute);
75
- }
76
- writeSuccess("✅ Skill compiled successfully");
22
+ if (fs.existsSync(distDir)) {
23
+ fs.rmSync(distDir, { recursive: true, force: true });
24
+ }
25
+ if (fs.existsSync(luaDir)) {
26
+ fs.rmSync(luaDir, { recursive: true, force: true });
27
+ }
28
+ // Create directory structures
29
+ fs.mkdirSync(distDir, { recursive: true });
30
+ fs.mkdirSync(path.join(distDir, "tools"), { recursive: true });
31
+ fs.mkdirSync(luaDir, { recursive: true });
32
+ // Find index.ts file
33
+ const indexPath = findIndexFile();
34
+ // Use ts-morph to analyze the TypeScript project
35
+ const project = new Project({
36
+ tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
37
+ });
38
+ // Add the index file to the project
39
+ const indexFile = project.addSourceFileAtPath(indexPath);
40
+ // Detect tools from skill.addTools calls
41
+ const tools = await detectTools(indexFile, project);
42
+ writeProgress(`📦 Found ${tools.length} tools to bundle...`);
43
+ // Bundle each tool individually and extract execute code
44
+ for (const tool of tools) {
45
+ await bundleTool(tool, distDir);
46
+ await extractExecuteCode(tool, project);
47
+ }
48
+ // Bundle the main index file
49
+ await bundleMainIndex(indexPath, distDir);
50
+ // Create both deployment formats
51
+ await createDeploymentData(tools, distDir);
52
+ await createLegacyDeploymentData(tools, luaDir, indexFile);
53
+ writeSuccess(`✅ Skill compiled successfully - ${tools.length} tools bundled`);
77
54
  }, "compilation");
78
55
  }
79
- async function extractSkillInfo(indexContent, indexFilePath) {
56
+ function findIndexFile() {
57
+ // Check for index.ts in current directory
58
+ let indexPath = path.join(process.cwd(), "index.ts");
59
+ if (fs.existsSync(indexPath)) {
60
+ return indexPath;
61
+ }
62
+ // Check for index.ts in src directory
63
+ indexPath = path.join(process.cwd(), "src", "index.ts");
64
+ if (fs.existsSync(indexPath)) {
65
+ return indexPath;
66
+ }
67
+ throw new Error("index.ts not found in current directory or src/ directory");
68
+ }
69
+ async function detectTools(indexFile, project) {
80
70
  const tools = [];
81
- const toolArguments = [];
82
- try {
83
- // Create a TypeScript source file
84
- const sourceFile = ts.createSourceFile(indexFilePath, indexContent, ts.ScriptTarget.Latest, true);
85
- // Traverse the AST to find skill.addTool() calls
86
- function visit(node) {
87
- // Check for skill.addTool() and skill.addTools() calls
88
- if (ts.isCallExpression(node) &&
89
- ts.isPropertyAccessExpression(node.expression) &&
90
- ts.isIdentifier(node.expression.expression) &&
91
- node.expression.expression.text === 'skill' &&
92
- ts.isIdentifier(node.expression.name) &&
93
- (node.expression.name.text === 'addTool' || node.expression.name.text === 'addTools')) {
94
- // Check if this call is commented out
95
- if (isNodeCommentedOut(node, sourceFile)) {
96
- return;
71
+ // Find tools in LuaSkill constructors
72
+ indexFile.forEachDescendant((node) => {
73
+ if (Node.isNewExpression(node)) {
74
+ const expression = node.getExpression();
75
+ if (expression.getText() === 'LuaSkill') {
76
+ const args = node.getArguments();
77
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
78
+ const configObj = args[0];
79
+ // Look for tools property in constructor
80
+ configObj.getProperties().forEach((prop) => {
81
+ if (Node.isPropertyAssignment(prop) && prop.getName() === 'tools') {
82
+ const value = prop.getInitializer();
83
+ if (value && Node.isArrayLiteralExpression(value)) {
84
+ const toolsArray = value;
85
+ toolsArray.getElements().forEach((element) => {
86
+ if (Node.isNewExpression(element)) {
87
+ const toolInfo = extractToolFromNewExpressionSync(element, project);
88
+ if (toolInfo) {
89
+ tools.push(toolInfo);
90
+ }
91
+ }
92
+ });
93
+ }
94
+ }
95
+ });
97
96
  }
98
- // Handle both addTool and addTools calls
99
- if (node.expression.name.text === 'addTool') {
100
- // Single tool call
101
- if (node.arguments.length > 0) {
102
- toolArguments.push(node.arguments[0]);
97
+ }
98
+ }
99
+ });
100
+ // Find all call expressions in the file (addTools method calls)
101
+ indexFile.forEachDescendant((node) => {
102
+ if (Node.isCallExpression(node)) {
103
+ const expression = node.getExpression();
104
+ // Check if this is skill.addTools or skill.addTool
105
+ if (Node.isPropertyAccessExpression(expression)) {
106
+ const object = expression.getExpression();
107
+ const property = expression.getName();
108
+ if (property === 'addTools' || property === 'addTool') {
109
+ const args = node.getArguments();
110
+ if (property === 'addTools' && args.length > 0) {
111
+ // Handle skill.addTools([...]) - array of tools
112
+ const arrayArg = args[0];
113
+ if (Node.isArrayLiteralExpression(arrayArg)) {
114
+ const elements = arrayArg.getElements();
115
+ for (const element of elements) {
116
+ if (Node.isNewExpression(element)) {
117
+ const toolInfo = extractToolFromNewExpressionSync(element, project);
118
+ if (toolInfo) {
119
+ tools.push(toolInfo);
120
+ }
121
+ }
122
+ }
123
+ }
103
124
  }
104
- }
105
- else if (node.expression.name.text === 'addTools') {
106
- // Multiple tools call - add each tool from the array
107
- if (node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0])) {
108
- for (const element of node.arguments[0].elements) {
109
- toolArguments.push(element);
125
+ else if (property === 'addTool' && args.length > 0) {
126
+ // Handle skill.addTool(new ToolClass()) - single tool
127
+ const arg = args[0];
128
+ if (Node.isNewExpression(arg)) {
129
+ const toolInfo = extractToolFromNewExpressionSync(arg, project);
130
+ if (toolInfo) {
131
+ tools.push(toolInfo);
132
+ }
110
133
  }
111
134
  }
112
135
  }
113
136
  }
114
- // Continue traversing
115
- ts.forEachChild(node, visit);
116
137
  }
117
- visit(sourceFile);
118
- // Process all collected tool arguments
119
- for (const toolArgument of toolArguments) {
120
- await processToolArgument(toolArgument, sourceFile, indexContent, tools);
121
- }
122
- }
123
- catch (error) {
124
- console.warn('Warning: Could not parse TypeScript AST:', error);
125
- return [];
126
- }
138
+ });
127
139
  return tools;
128
140
  }
129
- function isNodeCommentedOut(node, sourceFile) {
130
- const nodeStart = node.getStart(sourceFile);
131
- const nodeLine = sourceFile.getLineAndCharacterOfPosition(nodeStart).line;
132
- // Get the text of the line up to the node
133
- const lineStart = sourceFile.getPositionOfLineAndCharacter(nodeLine, 0);
134
- const lineText = sourceFile.text.substring(lineStart, nodeStart);
135
- // Check if the line starts with a comment
136
- const trimmedLine = lineText.trim();
137
- return trimmedLine.startsWith('//') ||
138
- trimmedLine.startsWith('*') ||
139
- trimmedLine.startsWith('#');
140
- }
141
- async function processToolArgument(toolArgument, sourceFile, indexContent, tools) {
142
- if (ts.isObjectLiteralExpression(toolArgument)) {
143
- // Inline tool definition: skill.addTool({...})
144
- await processInlineTool(toolArgument, sourceFile, indexContent, tools);
145
- }
146
- else if (ts.isNewExpression(toolArgument)) {
147
- // Class-based tool: skill.addTool(new SomeTool())
148
- await processClassBasedTool(toolArgument, sourceFile, indexContent, tools);
149
- }
150
- }
151
- async function processInlineTool(objectNode, sourceFile, indexContent, tools) {
152
- const toolProperties = {};
153
- // Extract properties from the object
154
- for (const property of objectNode.properties) {
155
- if (ts.isPropertyAssignment(property) &&
156
- ts.isIdentifier(property.name) &&
157
- property.initializer) {
158
- const key = property.name.text;
159
- const value = property.initializer;
160
- if (key === 'name' && ts.isStringLiteral(value)) {
161
- toolProperties.name = value.text;
162
- }
163
- else if (key === 'description' && ts.isStringLiteral(value)) {
164
- toolProperties.description = value.text;
165
- }
166
- else if (key === 'inputSchema' && ts.isIdentifier(value)) {
167
- toolProperties.inputSchemaVar = value.text;
168
- }
169
- else if (key === 'execute' && ts.isArrowFunction(value)) {
170
- // Extract the function body
171
- if (ts.isBlock(value.body)) {
172
- const bodyText = extractFunctionBody(value.body, sourceFile);
173
- toolProperties.executeBody = bodyText;
141
+ function extractToolFromNewExpressionSync(newExpr, project) {
142
+ try {
143
+ const expression = newExpr.getExpression();
144
+ const className = expression.getText();
145
+ // Find the import declaration for this class
146
+ const sourceFile = newExpr.getSourceFile();
147
+ const imports = sourceFile.getImportDeclarations();
148
+ for (const importDecl of imports) {
149
+ const namedImports = importDecl.getNamedImports();
150
+ const defaultImport = importDecl.getDefaultImport();
151
+ let toolFilePath = null;
152
+ // Check named imports
153
+ for (const namedImport of namedImports) {
154
+ if (namedImport.getName() === className) {
155
+ toolFilePath = resolveImportPath(importDecl.getModuleSpecifierValue(), sourceFile.getFilePath());
156
+ break;
174
157
  }
175
158
  }
176
- }
177
- }
178
- // Create the tool if all required properties are present
179
- if (toolProperties.name &&
180
- toolProperties.description &&
181
- toolProperties.inputSchemaVar &&
182
- toolProperties.executeBody) {
183
- // For inline tools, we need to find the schema definitions in the index file
184
- const inputSchema = extractSchemaFromIndex(toolProperties.inputSchemaVar, indexContent);
185
- const selfContainedExecute = await createSelfContainedExecute(toolProperties.executeBody, indexContent);
186
- tools.push({
187
- name: toolProperties.name,
188
- description: toolProperties.description,
189
- inputSchema: inputSchema || { type: "object" },
190
- execute: selfContainedExecute
191
- });
192
- }
193
- }
194
- async function processClassBasedTool(newNode, sourceFile, indexContent, tools) {
195
- if (ts.isIdentifier(newNode.expression)) {
196
- const className = newNode.expression.text;
197
- const toolInfo = await extractToolFromClass(className, indexContent, sourceFile.fileName);
198
- if (toolInfo) {
199
- tools.push(toolInfo);
200
- }
201
- }
202
- }
203
- function extractFunctionBody(blockStatement, sourceFile) {
204
- // Extract the function body text from the AST
205
- const start = blockStatement.getStart(sourceFile);
206
- const end = blockStatement.getEnd();
207
- // Get the text between the braces
208
- const fullText = sourceFile.text.substring(start, end);
209
- // Remove the outer braces and return the inner content
210
- if (fullText.startsWith('{') && fullText.endsWith('}')) {
211
- return fullText.slice(1, -1).trim();
212
- }
213
- return fullText;
214
- }
215
- function convertZodTypeToJSONSchema(zodType) {
216
- switch (zodType) {
217
- case 'string':
218
- return { type: 'string' };
219
- case 'number':
220
- return { type: 'number' };
221
- case 'boolean':
222
- return { type: 'boolean' };
223
- default:
224
- return { type: 'string' };
225
- }
226
- }
227
- function extractZodSchemaFromAST(node) {
228
- // Handle z.object({...}) calls
229
- if (ts.isCallExpression(node) &&
230
- ts.isPropertyAccessExpression(node.expression) &&
231
- ts.isIdentifier(node.expression.expression) &&
232
- node.expression.expression.text === 'z' &&
233
- ts.isIdentifier(node.expression.name) &&
234
- node.expression.name.text === 'object') {
235
- const objectLiteral = node.arguments[0];
236
- if (ts.isObjectLiteralExpression(objectLiteral)) {
237
- const properties = {};
238
- const required = [];
239
- for (const property of objectLiteral.properties) {
240
- if (ts.isPropertyAssignment(property) &&
241
- ts.isIdentifier(property.name) &&
242
- property.initializer) {
243
- const propertyName = property.name.text;
244
- const zodType = extractZodTypeFromAST(property.initializer);
245
- properties[propertyName] = zodType;
246
- // Only add to required if it's not optional
247
- if (!isOptionalZodType(property.initializer)) {
248
- required.push(propertyName);
249
- }
250
- }
159
+ // Check default import
160
+ if (!toolFilePath && defaultImport && defaultImport.getText() === className) {
161
+ toolFilePath = resolveImportPath(importDecl.getModuleSpecifierValue(), sourceFile.getFilePath());
251
162
  }
252
- return {
253
- type: "object",
254
- properties,
255
- required
256
- };
257
- }
258
- }
259
- return { type: "object" };
260
- }
261
- function extractZodTypeFromAST(node) {
262
- // Handle z.string(), z.number(), z.boolean(), etc.
263
- if (ts.isCallExpression(node) &&
264
- ts.isPropertyAccessExpression(node.expression) &&
265
- ts.isIdentifier(node.expression.expression) &&
266
- node.expression.expression.text === 'z') {
267
- const zodType = node.expression.name.text;
268
- switch (zodType) {
269
- case 'string':
270
- return { type: 'string' };
271
- case 'number':
272
- return { type: 'number' };
273
- case 'boolean':
274
- return { type: 'boolean' };
275
- case 'array':
276
- // Handle z.array(z.string()) etc.
277
- if (node.arguments.length > 0) {
278
- const elementType = extractZodTypeFromAST(node.arguments[0]);
279
- return { type: 'array', items: elementType };
280
- }
281
- return { type: 'array' };
282
- case 'object':
283
- // Handle nested z.object({...}) calls
284
- if (node.arguments.length > 0) {
285
- return extractZodSchemaFromAST(node);
286
- }
287
- return { type: 'object' };
288
- case 'enum':
289
- // Handle z.enum(['a', 'b', 'c'])
290
- if (node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0])) {
291
- const enumValues = [];
292
- for (const element of node.arguments[0].elements) {
293
- if (ts.isStringLiteral(element)) {
294
- enumValues.push(element.text);
163
+ if (toolFilePath && fs.existsSync(toolFilePath)) {
164
+ // Extract tool metadata from the class file
165
+ try {
166
+ const toolSourceFile = project.addSourceFileAtPath(toolFilePath);
167
+ const classDecl = toolSourceFile.getClass(className);
168
+ if (classDecl) {
169
+ const nameProperty = classDecl.getProperty('name');
170
+ const descProperty = classDecl.getProperty('description');
171
+ let toolName = className.replace(/Tool$/, '').toLowerCase();
172
+ let description = '';
173
+ // Extract name from property if available
174
+ if (nameProperty && nameProperty.getInitializer()) {
175
+ const nameValue = nameProperty.getInitializer()?.getText();
176
+ if (nameValue) {
177
+ toolName = nameValue.replace(/['"]/g, '');
178
+ }
295
179
  }
180
+ // Extract description from property if available
181
+ if (descProperty && descProperty.getInitializer()) {
182
+ const descValue = descProperty.getInitializer()?.getText();
183
+ if (descValue) {
184
+ description = descValue.replace(/['"]/g, '');
185
+ }
186
+ }
187
+ const toolInfo = {
188
+ name: toolName,
189
+ className,
190
+ filePath: toolFilePath,
191
+ description
192
+ };
193
+ return toolInfo;
296
194
  }
297
- return { type: 'string', enum: enumValues };
298
195
  }
299
- return { type: 'string' };
300
- default:
301
- return { type: 'string' };
302
- }
303
- }
304
- // Handle method chaining like z.string().optional()
305
- if (ts.isCallExpression(node) &&
306
- ts.isPropertyAccessExpression(node.expression) &&
307
- ts.isCallExpression(node.expression.expression)) {
308
- const baseType = extractZodTypeFromAST(node.expression.expression);
309
- const method = node.expression.name.text;
310
- if (method === 'optional') {
311
- // Optional fields are handled by not including them in required array
312
- return baseType;
313
- }
314
- return baseType;
315
- }
316
- return { type: 'string' };
317
- }
318
- function isOptionalZodType(node) {
319
- // Check if this is a chained call ending with .optional()
320
- if (ts.isCallExpression(node) &&
321
- ts.isPropertyAccessExpression(node.expression) &&
322
- ts.isIdentifier(node.expression.name) &&
323
- node.expression.name.text === 'optional') {
324
- return true;
325
- }
326
- return false;
327
- }
328
- function extractSchemaFromIndex(schemaVar, indexContent) {
329
- // Create a source file from the index content to parse schemas using AST
330
- const sourceFile = ts.createSourceFile('index.ts', indexContent, ts.ScriptTarget.Latest, true);
331
- let schema = null;
332
- function findSchema(node) {
333
- // Look for const schemaVar = z.object({...}) declarations
334
- if (ts.isVariableDeclaration(node) &&
335
- ts.isIdentifier(node.name) &&
336
- node.name.text === schemaVar &&
337
- node.initializer) {
338
- schema = extractZodSchemaFromAST(node.initializer);
339
- return;
340
- }
341
- ts.forEachChild(node, findSchema);
342
- }
343
- findSchema(sourceFile);
344
- return schema;
345
- }
346
- function extractEnvironmentVariables() {
347
- const config = readSkillConfig();
348
- const envVars = [];
349
- if (config?.skill?.env) {
350
- for (const [key, value] of Object.entries(config.skill.env)) {
351
- envVars.push({
352
- name: key,
353
- value: value
354
- });
355
- }
356
- }
357
- return envVars;
358
- }
359
- async function createSelfContainedExecute(executeBody, indexContent) {
360
- const dependencies = [];
361
- const bundledPackages = new Set();
362
- // 1. Parse external package imports and bundle their code
363
- const allImportRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
364
- let importMatch;
365
- while ((importMatch = allImportRegex.exec(indexContent)) !== null) {
366
- const namedImports = importMatch[1]; // Named imports like { z }
367
- const defaultImport = importMatch[2]; // Default import like axios
368
- const packagePath = importMatch[3];
369
- // Skip local imports (relative paths)
370
- if (packagePath.startsWith('./') || packagePath.startsWith('../')) {
371
- continue;
372
- }
373
- // Skip lua-cli imports (these are handled separately)
374
- if (packagePath.startsWith('lua-cli')) {
375
- continue;
376
- }
377
- // Skip zod - assume it's always available on target machine
378
- if (packagePath === 'zod') {
379
- // Add require statement for zod instead of bundling
380
- if (namedImports) {
381
- const importsList = namedImports.split(',').map(imp => imp.trim());
382
- const usedImports = importsList.filter(imp => executeBody.includes(imp) || indexContent.includes(`${imp}.`));
383
- if (usedImports.length > 0) {
384
- const requireStatement = usedImports.length === 1
385
- ? `const { ${usedImports[0]} } = require('zod');`
386
- : `const { ${usedImports.join(', ')} } = require('zod');`;
387
- bundledPackages.add(requireStatement);
196
+ catch (fileError) {
197
+ console.warn(`Warning: Could not load tool file ${toolFilePath}:`, fileError);
388
198
  }
389
199
  }
390
- else if (defaultImport) {
391
- bundledPackages.add(`const ${defaultImport} = require('zod');`);
392
- }
393
- continue;
394
- }
395
- // Bundle other external packages
396
- if (namedImports || defaultImport) {
397
- const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
398
- if (packageCode) {
399
- bundledPackages.add(packageCode);
400
- }
401
- }
402
- }
403
- // 2. Extract class definitions with proper brace matching
404
- const classRegex = /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g;
405
- let classMatch;
406
- while ((classMatch = classRegex.exec(indexContent)) !== null) {
407
- const className = classMatch[1];
408
- const classStart = classMatch.index;
409
- // Find the matching closing brace
410
- let braceCount = 0;
411
- let classEnd = classStart;
412
- let found = false;
413
- for (let i = classStart; i < indexContent.length; i++) {
414
- if (indexContent[i] === '{') {
415
- braceCount++;
416
- }
417
- else if (indexContent[i] === '}') {
418
- braceCount--;
419
- if (braceCount === 0) {
420
- classEnd = i;
421
- found = true;
422
- break;
423
- }
424
- }
425
- }
426
- if (found) {
427
- const fullClass = indexContent.substring(classStart, classEnd + 1);
428
- // Check if this class is used in the execute function
429
- let isUsed = false;
430
- // Direct usage in execute body
431
- if (executeBody.includes(`new ${className}`) ||
432
- executeBody.includes(`${className}.`) ||
433
- executeBody.includes(`${className}(`)) {
434
- isUsed = true;
435
- }
436
- // Check if any variable that uses this class is referenced in execute body
437
- const variableRegex = new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${className}\\s*\\([^)]*\\);`, 'g');
438
- let varMatch;
439
- while ((varMatch = variableRegex.exec(indexContent)) !== null) {
440
- const varName = varMatch[1];
441
- if (executeBody.includes(varName)) {
442
- isUsed = true;
443
- break;
444
- }
445
- }
446
- if (isUsed) {
447
- dependencies.push(fullClass);
448
- }
449
200
  }
201
+ return null;
450
202
  }
451
- // 3. Extract function definitions
452
- const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\}/g;
453
- let functionMatch;
454
- while ((functionMatch = functionRegex.exec(indexContent)) !== null) {
455
- const functionName = functionMatch[1];
456
- const functionBody = functionMatch[2];
457
- if (executeBody.includes(functionName)) {
458
- dependencies.push(`function ${functionName}() {\n${functionBody}\n}`);
459
- }
203
+ catch (error) {
204
+ console.warn(`Warning: Could not extract tool info for ${newExpr.getText()}:`, error);
205
+ return null;
460
206
  }
461
- // 4. Extract const/let/var declarations (avoid duplicates)
462
- const varRegex = /(?:const|let|var)\s+(\w+)\s*=\s*([^;]+);/g;
463
- const declaredVars = new Set();
464
- let varMatch;
465
- while ((varMatch = varRegex.exec(indexContent)) !== null) {
466
- const varName = varMatch[1];
467
- const varValue = varMatch[2];
468
- // Skip if it's a class instantiation (we'll handle that separately)
469
- if (varValue.includes('new ') && varValue.includes('()')) {
470
- continue;
471
- }
472
- // Skip if already declared
473
- if (declaredVars.has(varName)) {
474
- continue;
475
- }
476
- if (executeBody.includes(varName)) {
477
- declaredVars.add(varName);
478
- dependencies.push(`const ${varName} = ${varValue};`);
479
- }
207
+ }
208
+ function resolveImportPath(moduleSpecifier, currentFilePath) {
209
+ if (moduleSpecifier.startsWith('./') || moduleSpecifier.startsWith('../')) {
210
+ // Relative import - resolve relative to current file
211
+ const currentDir = path.dirname(currentFilePath);
212
+ return path.resolve(currentDir, moduleSpecifier + '.ts');
480
213
  }
481
- // 5. Extract class instantiations (avoid duplicates)
482
- const instantiationRegex = /(?:const|let|var)\s+(\w+)\s*=\s*new\s+(\w+)\([^)]*\);/g;
483
- let instantiationMatch;
484
- while ((instantiationMatch = instantiationRegex.exec(indexContent)) !== null) {
485
- const instanceName = instantiationMatch[1];
486
- const className = instantiationMatch[2];
487
- // Skip if already declared
488
- if (declaredVars.has(instanceName)) {
489
- continue;
490
- }
491
- if (executeBody.includes(instanceName)) {
492
- declaredVars.add(instanceName);
493
- dependencies.push(`const ${instanceName} = new ${className}();`);
494
- }
214
+ else {
215
+ // Absolute import - assume it's in the project
216
+ return path.resolve(process.cwd(), 'src', moduleSpecifier + '.ts');
495
217
  }
496
- // 6. Create the self-contained execute function
497
- const allDependencies = [...Array.from(bundledPackages), ...dependencies];
498
- const dependencyCode = allDependencies.join('\n');
499
- // Strip TypeScript type annotations for JavaScript compatibility (only for local code)
500
- const cleanDependencyCode = allDependencies.map(dep => {
501
- // Only strip TypeScript from local dependencies, not bundled packages
502
- if (dep.includes('require(') || dep.includes('import ')) {
503
- return dep; // Skip bundled packages
504
- }
505
- return dep
506
- .replace(/:\s*string/g, '') // Remove : string
507
- .replace(/:\s*number/g, '') // Remove : number
508
- .replace(/:\s*boolean/g, '') // Remove : boolean
509
- .replace(/:\s*any/g, '') // Remove : any
510
- .replace(/:\s*void/g, '') // Remove : void
511
- .replace(/:\s*object/g, '') // Remove : object
512
- .replace(/:\s*Array<[^>]+>/g, '') // Remove : Array<Type>
513
- .replace(/:\s*Promise<[^>]+>/g, '') // Remove : Promise<Type>
514
- .replace(/:\s*Record<[^>]+>/g, ''); // Remove : Record<Type>
515
- }).join('\n');
516
- const cleanExecuteBody = executeBody
517
- .replace(/:\s*string/g, '') // Remove : string
518
- .replace(/:\s*number/g, '') // Remove : number
519
- .replace(/:\s*boolean/g, '') // Remove : boolean
520
- .replace(/:\s*any/g, '') // Remove : any
521
- .replace(/:\s*void/g, '') // Remove : void
522
- .replace(/:\s*object/g, '') // Remove : object
523
- .replace(/:\s*Array<[^>]+>/g, '') // Remove : Array<Type>
524
- .replace(/:\s*Promise<[^>]+>/g, '') // Remove : Promise<Type>
525
- .replace(/:\s*Record<[^>]+>/g, ''); // Remove : Record<Type>
526
- const selfContainedExecute = `async (input) => {
527
- ${cleanDependencyCode ? ` ${cleanDependencyCode.split('\n').join('\n ')}\n` : ''} ${cleanExecuteBody.trim()}
528
- }`;
529
- return selfContainedExecute;
530
218
  }
531
- async function bundlePackageCode(packagePath, namedImports, defaultImport) {
219
+ async function bundleTool(tool, distDir) {
220
+ writeProgress(`📦 Bundling ${tool.className}...`);
532
221
  try {
533
- const { build } = await import('esbuild');
534
- // Create a temporary entry file for esbuild in .lua directory
535
- const luaDir = path.join(process.cwd(), '.lua');
536
- if (!fs.existsSync(luaDir)) {
537
- fs.mkdirSync(luaDir, { recursive: true });
538
- }
539
- const entryFile = path.join(luaDir, `${packagePath}-entry.cjs`);
540
- const outputFile = path.join(luaDir, `${packagePath}-bundle.cjs`);
541
- // Create entry file based on import type
542
- let entryContent = '';
543
- if (defaultImport) {
544
- // For default imports like `import axios from 'axios'`
545
- entryContent = `import ${defaultImport} from '${packagePath}';\nmodule.exports = ${defaultImport};`;
546
- }
547
- else if (namedImports) {
548
- // For named imports like `import { z } from 'zod'`
549
- const importsList = namedImports.split(',').map(imp => imp.trim());
550
- entryContent = `import { ${importsList.join(', ')} } from '${packagePath}';\nmodule.exports = { ${importsList.join(', ')} };`;
551
- }
552
- else {
553
- // Fallback - import everything
554
- entryContent = `import * as ${packagePath.replace(/[^a-zA-Z0-9]/g, '_')} from '${packagePath}';\nmodule.exports = ${packagePath.replace(/[^a-zA-Z0-9]/g, '_')};`;
555
- }
556
- // Write entry file
557
- fs.writeFileSync(entryFile, entryContent);
558
- // Bundle with esbuild
559
- const result = await build({
560
- entryPoints: [entryFile],
222
+ const outputPath = path.join(distDir, 'tools', `${tool.className}.js`);
223
+ await build({
224
+ entryPoints: [tool.filePath],
561
225
  bundle: true,
562
- format: 'cjs', // CommonJS format
226
+ format: 'cjs',
563
227
  platform: 'node',
564
228
  target: 'node16',
565
- outfile: outputFile,
566
- external: [], // Bundle everything
567
- minify: false, // Keep readable for debugging
229
+ outfile: outputPath,
230
+ external: ['lua-cli/skill', 'lua-cli/user-data-api', 'zod'], // Exclude lua-cli modules and zod - injected into VM
231
+ minify: true, // Minify for smaller file sizes
568
232
  sourcemap: false,
569
- write: true,
570
- resolveExtensions: ['.js', '.ts', '.json'],
571
- mainFields: ['main', 'module', 'browser'],
572
- conditions: ['node'],
573
- nodePaths: [
574
- path.join(process.cwd(), 'node_modules'),
575
- path.join(process.cwd(), '..', 'node_modules'),
576
- path.join(process.cwd(), '..', '..', 'node_modules')
577
- ],
578
- absWorkingDir: process.cwd(),
233
+ resolveExtensions: ['.ts', '.js', '.json'],
234
+ define: {
235
+ 'process.env.NODE_ENV': '"production"'
236
+ },
237
+ // Ensure all other dependencies are bundled
238
+ packages: 'bundle',
239
+ // Handle different module formats
240
+ mainFields: ['main', 'module'],
241
+ conditions: ['node']
579
242
  });
580
- if (result.errors.length > 0) {
581
- console.warn(`Warning: esbuild errors for package ${packagePath}:`, result.errors);
582
- return null;
583
- }
584
- // Read the bundled output
585
- if (!fs.existsSync(outputFile)) {
586
- console.warn(`Warning: Bundle output not found for package ${packagePath}`);
587
- return null;
588
- }
589
- const bundledContent = fs.readFileSync(outputFile, 'utf8');
590
- // Clean up temporary files
591
- try {
592
- fs.unlinkSync(entryFile);
593
- fs.unlinkSync(outputFile);
594
- }
595
- catch (cleanupError) {
596
- // Ignore cleanup errors
597
- }
598
- // Create the final bundled code
599
- let finalCode = '';
600
- if (defaultImport) {
601
- finalCode = `const ${defaultImport} = (function() {\n${bundledContent}\n return module.exports;\n})();\n`;
602
- }
603
- else if (namedImports) {
604
- const importsList = namedImports.split(',').map(imp => imp.trim());
605
- finalCode = `(function() {\n${bundledContent}\n})();\n`;
606
- finalCode += `const { ${importsList.join(', ')} } = module.exports;\n`;
607
- }
608
- else {
609
- finalCode = `(function() {\n${bundledContent}\n})();\n`;
610
- }
611
- return finalCode;
243
+ // Add VM-compatible wrapper
244
+ await wrapToolForVM(outputPath, tool);
612
245
  }
613
246
  catch (error) {
614
- console.warn(`Warning: Could not bundle package ${packagePath} with esbuild:`, error);
615
- // For test environments or when esbuild fails, provide a fallback
616
- if (packagePath === 'axios') {
617
- return createWorkingAxiosImplementation();
618
- }
619
- return null;
247
+ console.warn(`Warning: Failed to bundle ${tool.className}:`, error);
620
248
  }
621
249
  }
622
- function createWorkingAxiosImplementation() {
623
- return `
624
- // Working axios implementation using native fetch (for test environments)
625
- const axios = {
626
- get: async (url, config = {}) => {
627
- const searchParams = new URLSearchParams(config.params || {});
628
- const fullUrl = searchParams.toString() ? \`\${url}?\${searchParams}\` : url;
629
-
630
- const response = await fetch(fullUrl, {
631
- method: 'GET',
632
- headers: {
633
- 'Content-Type': 'application/json',
634
- ...config.headers
250
+ async function wrapToolForVM(outputPath, tool) {
251
+ const bundledCode = fs.readFileSync(outputPath, 'utf8');
252
+ // Create a wrapper that's compatible with the existing sandbox.ts VM system
253
+ // The sandbox expects: const executeFunction = ${toolCode}; module.exports = async (input) => { return await executeFunction(input); };
254
+ const wrappedCode = `async (input) => {
255
+ // Mock lua-cli/skill module for external dependencies
256
+ const luaCliSkill = {
257
+ env: function(key) {
258
+ if (typeof env !== 'undefined') {
259
+ return env(key);
635
260
  }
636
- });
637
-
638
- if (!response.ok) {
639
- const error = new Error(\`Request failed with status \${response.status}\`);
640
- error.response = { status: response.status, statusText: response.statusText };
641
- throw error;
642
- }
643
-
644
- const data = await response.json();
645
- return {
646
- data,
647
- status: response.status,
648
- statusText: response.statusText,
649
- headers: response.headers,
650
- config: config
651
- };
652
- },
653
-
654
- post: async (url, data, config = {}) => {
655
- const response = await fetch(url, {
656
- method: 'POST',
657
- headers: {
658
- 'Content-Type': 'application/json',
659
- ...config.headers
660
- },
661
- body: JSON.stringify(data)
662
- });
663
-
664
- if (!response.ok) {
665
- const error = new Error(\`Request failed with status \${response.status}\`);
666
- error.response = { status: response.status, statusText: response.statusText };
667
- throw error;
261
+ return process.env[key] || '';
668
262
  }
669
-
670
- const responseData = await response.json();
671
- return {
672
- data: responseData,
673
- status: response.status,
674
- statusText: response.statusText,
675
- headers: response.headers,
676
- config: config
677
- };
678
- },
263
+ };
679
264
 
680
- put: async (url, data, config = {}) => {
681
- const response = await fetch(url, {
682
- method: 'PUT',
683
- headers: {
684
- 'Content-Type': 'application/json',
685
- ...config.headers
686
- },
687
- body: JSON.stringify(data)
688
- });
689
-
690
- if (!response.ok) {
691
- const error = new Error(\`Request failed with status \${response.status}\`);
692
- error.response = { status: response.status, statusText: response.statusText };
693
- throw error;
265
+ // Mock lua-cli/user-data-api module
266
+ const luaCliUserDataApi = {
267
+ user: typeof user !== 'undefined' ? user : {
268
+ data: {
269
+ get: async () => ({}),
270
+ update: async (data) => data,
271
+ create: async (data) => data
272
+ }
694
273
  }
695
-
696
- const responseData = await response.json();
697
- return {
698
- data: responseData,
699
- status: response.status,
700
- statusText: response.statusText,
701
- headers: response.headers,
702
- config: config
703
- };
704
- },
274
+ };
705
275
 
706
- delete: async (url, config = {}) => {
707
- const response = await fetch(url, {
708
- method: 'DELETE',
709
- headers: {
710
- 'Content-Type': 'application/json',
711
- ...config.headers
276
+ // Mock zod module
277
+ const zodModule = (() => {
278
+ try {
279
+ if (typeof require !== 'undefined') {
280
+ return require('zod');
712
281
  }
713
- });
714
-
715
- if (!response.ok) {
716
- const error = new Error(\`Request failed with status \${response.status}\`);
717
- error.response = { status: response.status, statusText: response.statusText };
718
- throw error;
282
+ } catch (e) {
283
+ // Fallback zod implementation
719
284
  }
720
-
721
- const responseData = await response.json();
722
- return {
723
- data: responseData,
724
- status: response.status,
725
- statusText: response.statusText,
726
- headers: response.headers,
727
- config: config
285
+ return {
286
+ z: {
287
+ object: (schema) => ({
288
+ parse: (data) => data,
289
+ safeParse: (data) => ({ success: true, data })
290
+ }),
291
+ string: () => ({
292
+ parse: (data) => String(data),
293
+ safeParse: (data) => ({ success: true, data: String(data) })
294
+ }),
295
+ number: () => ({
296
+ parse: (data) => Number(data),
297
+ safeParse: (data) => ({ success: true, data: Number(data) })
298
+ }),
299
+ boolean: () => ({
300
+ parse: (data) => Boolean(data),
301
+ safeParse: (data) => ({ success: true, data: Boolean(data) })
302
+ })
303
+ }
728
304
  };
729
- },
305
+ })();
730
306
 
731
- patch: async (url, data, config = {}) => {
732
- const response = await fetch(url, {
733
- method: 'PATCH',
734
- headers: {
735
- 'Content-Type': 'application/json',
736
- ...config.headers
737
- },
738
- body: JSON.stringify(data)
739
- });
740
-
741
- if (!response.ok) {
742
- const error = new Error(\`Request failed with status \${response.status}\`);
743
- error.response = { status: response.status, statusText: response.statusText };
744
- throw error;
307
+ // Override require for external modules during execution
308
+ const originalRequire = require;
309
+ require = function(id) {
310
+ if (id === 'lua-cli/skill') {
311
+ return luaCliSkill;
745
312
  }
746
-
747
- const responseData = await response.json();
748
- return {
749
- data: responseData,
750
- status: response.status,
751
- statusText: response.statusText,
752
- headers: response.headers,
753
- config: config
754
- };
755
- }
756
- };
757
- `;
758
- }
759
- async function extractToolFromClass(className, indexContent, indexFilePath) {
760
- // Find the import statement for this class using AST
761
- const sourceFile = ts.createSourceFile(indexFilePath, indexContent, ts.ScriptTarget.Latest, true);
762
- let importPath = null;
763
- function findImport(node) {
764
- if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
765
- const moduleSpecifier = node.moduleSpecifier;
766
- // Check if this import has the class we're looking for
767
- if (node.importClause && node.importClause.name && node.importClause.name.text === className) {
768
- importPath = moduleSpecifier.text;
769
- return;
770
- }
771
- // Check named imports
772
- if (node.importClause && node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
773
- for (const element of node.importClause.namedBindings.elements) {
774
- if (element.name.text === className) {
775
- importPath = moduleSpecifier.text;
776
- return;
777
- }
778
- }
779
- }
780
- }
781
- ts.forEachChild(node, findImport);
313
+ if (id === 'lua-cli/user-data-api') {
314
+ return luaCliUserDataApi;
782
315
  }
783
- findImport(sourceFile);
784
- if (!importPath) {
785
- console.warn(`Warning: Could not find import for class ${className}`);
786
- return null;
787
- }
788
- // Read the tool file - handle both relative and absolute paths
789
- let toolFilePath;
790
- if (importPath.startsWith('./') || importPath.startsWith('../')) {
791
- // Relative path - resolve from the index file directory
792
- const indexDir = path.dirname(indexFilePath);
793
- toolFilePath = path.join(indexDir, importPath + '.ts');
316
+ if (id === 'zod') {
317
+ return zodModule;
794
318
  }
795
- else {
796
- // Absolute path or just filename
797
- toolFilePath = path.join(process.cwd(), importPath + '.ts');
319
+ return originalRequire(id);
320
+ };
321
+
322
+ // Execute the bundled tool code
323
+ ${bundledCode}
324
+
325
+ // Restore original require
326
+ require = originalRequire;
327
+
328
+ // Get the tool class from exports
329
+ const ToolClass = module.exports.default || module.exports.${tool.className} || module.exports;
330
+
331
+ // Create and execute the tool
332
+ const toolInstance = new ToolClass();
333
+ return await toolInstance.execute(input);
334
+ }`;
335
+ fs.writeFileSync(outputPath, wrappedCode);
336
+ }
337
+ async function bundleMainIndex(indexPath, distDir) {
338
+ writeProgress("📦 Bundling main index...");
339
+ try {
340
+ await build({
341
+ entryPoints: [indexPath],
342
+ bundle: true,
343
+ format: 'cjs',
344
+ platform: 'node',
345
+ target: 'node16',
346
+ outfile: path.join(distDir, 'index.js'),
347
+ external: ['lua-cli/skill', 'lua-cli/user-data-api', 'zod'], // Exclude lua-cli modules and zod
348
+ minify: true, // Minify for smaller file sizes
349
+ sourcemap: false,
350
+ resolveExtensions: ['.ts', '.js', '.json'],
351
+ define: {
352
+ 'process.env.NODE_ENV': '"production"'
353
+ },
354
+ packages: 'bundle',
355
+ mainFields: ['main', 'module'],
356
+ conditions: ['node']
357
+ });
798
358
  }
799
- if (!fs.existsSync(toolFilePath)) {
800
- console.warn(`Warning: Tool file not found: ${toolFilePath}`);
801
- return null;
359
+ catch (error) {
360
+ console.warn("Warning: Failed to bundle main index:", error);
802
361
  }
803
- const toolContent = fs.readFileSync(toolFilePath, 'utf8');
804
- const toolSourceFile = ts.createSourceFile(toolFilePath, toolContent, ts.ScriptTarget.Latest, true);
805
- // Extract tool properties using AST
806
- let toolName = null;
807
- let toolDescription = null;
808
- let inputSchema = null;
809
- let executeMethod = null;
810
- function extractToolInfo(node) {
811
- // Look for class declaration
812
- if (ts.isClassDeclaration(node) && node.name && node.name.text === className) {
813
- // Extract class members
814
- for (const member of node.members) {
815
- if (ts.isPropertyDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
816
- const propertyName = member.name.text;
817
- // Extract name property
818
- if (propertyName === 'name' && member.initializer && ts.isStringLiteral(member.initializer)) {
819
- toolName = member.initializer.text;
820
- }
821
- // Extract description property
822
- if (propertyName === 'description' && member.initializer && ts.isStringLiteral(member.initializer)) {
823
- toolDescription = member.initializer.text;
824
- }
825
- // Extract inputSchema property
826
- if (propertyName === 'inputSchema' && member.initializer) {
827
- inputSchema = extractZodSchemaFromAST(member.initializer);
828
- }
362
+ }
363
+ async function extractExecuteCode(tool, project) {
364
+ try {
365
+ const toolSourceFile = project.getSourceFile(tool.filePath);
366
+ if (!toolSourceFile) {
367
+ console.warn(`Warning: Could not find source file for ${tool.className}`);
368
+ return;
369
+ }
370
+ const classDecl = toolSourceFile.getClass(tool.className);
371
+ if (!classDecl) {
372
+ console.warn(`Warning: Could not find class ${tool.className} in ${tool.filePath}`);
373
+ return;
374
+ }
375
+ // Extract the execute method
376
+ const executeMethod = classDecl.getMethod('execute');
377
+ if (!executeMethod) {
378
+ console.warn(`Warning: Could not find execute method in ${tool.className}`);
379
+ return;
380
+ }
381
+ // Get the execute method body
382
+ const executeBody = executeMethod.getBodyText();
383
+ if (!executeBody) {
384
+ console.warn(`Warning: Execute method has no body in ${tool.className}`);
385
+ return;
386
+ }
387
+ // Read the bundled tool file to get the self-contained code
388
+ const bundledPath = path.join(process.cwd(), 'dist', 'tools', `${tool.className}.js`);
389
+ if (fs.existsSync(bundledPath)) {
390
+ const bundledCode = fs.readFileSync(bundledPath, 'utf8');
391
+ // Extract just the tool execution function from the bundled code
392
+ // The bundled code contains the VM wrapper, we need to extract the core functionality
393
+ const executeFunction = createExecuteFunction(bundledCode, tool);
394
+ tool.executeCode = executeFunction;
395
+ }
396
+ // Extract input schema from zod definition using proper library
397
+ const inputSchemaProperty = classDecl.getProperty('inputSchema');
398
+ if (inputSchemaProperty && inputSchemaProperty.getInitializer()) {
399
+ const initializer = inputSchemaProperty.getInitializer();
400
+ if (initializer) {
401
+ try {
402
+ // Evaluate the Zod schema to get the actual schema object
403
+ const zodSchemaCode = initializer.getText();
404
+ tool.inputSchema = await evaluateZodSchemaToJsonSchema(zodSchemaCode);
829
405
  }
830
- // Extract execute method
831
- if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name) && member.name.text === 'execute') {
832
- executeMethod = member;
406
+ catch (error) {
407
+ console.warn(`Warning: Could not parse Zod schema for ${tool.className}:`, error);
408
+ tool.inputSchema = { type: "object" };
833
409
  }
834
410
  }
835
411
  }
836
- ts.forEachChild(node, extractToolInfo);
837
- }
838
- extractToolInfo(toolSourceFile);
839
- if (!toolName || !toolDescription) {
840
- console.warn(`Warning: Could not extract name or description from ${className}`);
841
- return null;
412
+ if (!tool.inputSchema) {
413
+ tool.inputSchema = { type: "object" };
414
+ }
842
415
  }
843
- if (!inputSchema) {
844
- console.warn(`Warning: Could not find input schema in ${className}`);
845
- return null;
416
+ catch (error) {
417
+ console.warn(`Warning: Could not extract execute code for ${tool.className}:`, error);
846
418
  }
847
- if (!executeMethod) {
848
- console.warn(`Warning: Could not find execute method in ${className}`);
849
- return null;
419
+ }
420
+ function createExecuteFunction(bundledCode, tool) {
421
+ // The bundled code is already wrapped for VM execution by wrapToolForVM
422
+ // Just return it directly since it's already an async function
423
+ return bundledCode;
424
+ }
425
+ async function evaluateZodSchemaToJsonSchema(zodSchemaCode) {
426
+ try {
427
+ // Import zod and zod-to-json-schema dynamically
428
+ const { z } = await import('zod');
429
+ const { zodToJsonSchema } = await import('zod-to-json-schema');
430
+ // Create a safe evaluation context using Function constructor
431
+ const evalFunction = new Function('z', `return ${zodSchemaCode}`);
432
+ const zodSchema = evalFunction(z);
433
+ // Convert to JSON Schema using the library
434
+ const jsonSchema = zodToJsonSchema(zodSchema, 'schema');
435
+ // Extract just the core schema, removing JSON Schema references and definitions
436
+ if (jsonSchema.$ref && jsonSchema.definitions && jsonSchema.definitions.schema) {
437
+ // Return just the schema definition without the wrapper
438
+ return jsonSchema.definitions.schema;
439
+ }
440
+ // Remove the top-level $schema and title properties that we don't need
441
+ const { $schema, title, definitions, $ref, ...cleanSchema } = jsonSchema;
442
+ return cleanSchema;
850
443
  }
851
- // Extract execute method body
852
- if (!executeMethod.body) {
853
- console.warn(`Warning: Execute method has no body in ${className}`);
854
- return null;
444
+ catch (error) {
445
+ console.warn('Warning: Could not evaluate Zod schema, falling back to basic parsing:', error);
446
+ // Fallback to basic parsing for simple cases
447
+ if (zodSchemaCode.includes('z.object({')) {
448
+ return { type: 'object' };
449
+ }
450
+ if (zodSchemaCode.includes('z.string()'))
451
+ return { type: 'string' };
452
+ if (zodSchemaCode.includes('z.number()'))
453
+ return { type: 'number' };
454
+ if (zodSchemaCode.includes('z.boolean()'))
455
+ return { type: 'boolean' };
456
+ return { type: 'object' };
855
457
  }
856
- const executeBody = extractFunctionBody(executeMethod.body, toolSourceFile);
857
- // For class-based tools, we need to create a self-contained function that includes:
858
- // 1. The service classes
859
- // 2. Class instantiation
860
- // 3. The execute logic
861
- const selfContainedExecute = await createClassBasedExecute(executeBody, toolContent, className, toolFilePath);
862
- return {
863
- name: toolName,
864
- description: toolDescription,
865
- inputSchema: inputSchema || { type: "object" },
866
- execute: selfContainedExecute
458
+ }
459
+ async function createDeploymentData(tools, distDir) {
460
+ const config = readSkillConfig();
461
+ const packageJsonPath = path.join(process.cwd(), "package.json");
462
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
463
+ const deploymentData = {
464
+ name: config?.skill?.name || packageJson.name || "lua-skill",
465
+ version: config?.skill?.version || packageJson.version || "1.0.0",
466
+ skillId: config?.skill?.skillId || "",
467
+ description: config?.skill?.description || packageJson.description || "",
468
+ context: config?.skill?.context || "",
469
+ tools: tools.map(tool => ({
470
+ name: tool.name,
471
+ className: tool.className,
472
+ description: tool.description || "",
473
+ filePath: `tools/${tool.className}.js`
474
+ }))
867
475
  };
476
+ fs.writeFileSync(path.join(distDir, 'deployment.json'), JSON.stringify(deploymentData, null, 2));
868
477
  }
869
- async function createClassBasedExecute(executeBody, toolContent, className, toolFilePath) {
870
- const dependencies = [];
871
- const bundledPackages = new Set();
872
- // 1. Parse imports from the tool file
873
- const importRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
874
- let importMatch;
875
- while ((importMatch = importRegex.exec(toolContent)) !== null) {
876
- const namedImports = importMatch[1];
877
- const defaultImport = importMatch[2];
878
- const packagePath = importMatch[3];
879
- // Skip lua-cli imports
880
- if (packagePath.startsWith('lua-cli')) {
881
- continue;
882
- }
883
- // Handle zod
884
- if (packagePath === 'zod') {
885
- if (namedImports) {
886
- const importsList = namedImports.split(',').map(imp => imp.trim());
887
- const usedImports = importsList.filter(imp => executeBody.includes(imp) || toolContent.includes(`${imp}.`));
888
- if (usedImports.length > 0) {
889
- const requireStatement = usedImports.length === 1
890
- ? `const { ${usedImports[0]} } = require('zod');`
891
- : `const { ${usedImports.join(', ')} } = require('zod');`;
892
- bundledPackages.add(requireStatement);
478
+ async function createLegacyDeploymentData(tools, luaDir, indexFile) {
479
+ const config = readSkillConfig();
480
+ const packageJsonPath = path.join(process.cwd(), "package.json");
481
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
482
+ // Extract skills metadata from index.ts
483
+ const skillsMetadata = extractSkillsMetadata(indexFile);
484
+ // Group tools by skill based on addTools calls
485
+ const skillToTools = new Map();
486
+ // Find addTools calls to associate tools with skills
487
+ indexFile.forEachDescendant((node) => {
488
+ if (Node.isCallExpression(node)) {
489
+ const expression = node.getExpression();
490
+ if (Node.isPropertyAccessExpression(expression) && expression.getName() === 'addTools') {
491
+ const object = expression.getExpression();
492
+ const objectName = object.getText();
493
+ // Get the tools array
494
+ const args = node.getArguments();
495
+ if (args.length > 0 && Node.isArrayLiteralExpression(args[0])) {
496
+ const toolsArray = args[0];
497
+ const toolNames = toolsArray.getElements().map((element) => {
498
+ if (Node.isNewExpression(element)) {
499
+ return element.getExpression().getText();
500
+ }
501
+ return '';
502
+ }).filter(name => name);
503
+ skillToTools.set(objectName, toolNames);
893
504
  }
894
505
  }
895
- else if (defaultImport) {
896
- bundledPackages.add(`const ${defaultImport} = require('zod');`);
897
- }
898
- continue;
899
506
  }
900
- // Handle axios - bundle it properly
901
- if (packagePath === 'axios') {
902
- const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
903
- if (packageCode) {
904
- bundledPackages.add(packageCode);
507
+ });
508
+ // Create skills array with their associated tools
509
+ const skillsArray = skillsMetadata.map(skillMeta => {
510
+ let skillTools = [];
511
+ // First, check for tools from constructor
512
+ if (skillMeta.constructorTools && skillMeta.constructorTools.length > 0) {
513
+ skillTools = tools.filter(tool => {
514
+ return skillMeta.constructorTools.includes(tool.className) ||
515
+ skillMeta.constructorTools.includes(tool.name);
516
+ });
517
+ }
518
+ else {
519
+ // Find tools for this skill by matching variable names from addTools calls
520
+ for (const [varName, toolNames] of skillToTools.entries()) {
521
+ // Simple heuristic: if variable name contains skill name, associate tools
522
+ if (varName.toLowerCase().includes(skillMeta.name.toLowerCase().replace('-skill', '')) ||
523
+ varName.toLowerCase().includes(skillMeta.name.toLowerCase())) {
524
+ skillTools = tools.filter(tool => {
525
+ return toolNames.includes(tool.className) || toolNames.includes(tool.name);
526
+ });
527
+ break;
528
+ }
905
529
  }
906
- continue;
907
530
  }
908
- // Handle local service imports
909
- if (packagePath.startsWith('./') || packagePath.startsWith('../')) {
910
- // Resolve the service file path relative to the tool file location
911
- const toolDir = path.dirname(toolFilePath);
912
- const serviceFilePath = path.resolve(toolDir, packagePath + '.ts');
913
- if (fs.existsSync(serviceFilePath)) {
914
- const serviceContent = fs.readFileSync(serviceFilePath, 'utf8');
915
- // Process all imports in the service file
916
- const serviceImportRegex = /import\s+(?:(?:\{([^}]+)\})|(\w+))\s+from\s+["']([^"']+)["']/g;
917
- let serviceImportMatch;
918
- while ((serviceImportMatch = serviceImportRegex.exec(serviceContent)) !== null) {
919
- const namedImports = serviceImportMatch[1];
920
- const defaultImport = serviceImportMatch[2];
921
- const packagePath = serviceImportMatch[3];
922
- // Skip lua-cli imports
923
- if (packagePath.startsWith('lua-cli')) {
924
- continue;
925
- }
926
- // Handle zod
927
- if (packagePath === 'zod') {
928
- if (namedImports) {
929
- const importsList = namedImports.split(',').map(imp => imp.trim());
930
- const usedImports = importsList.filter(imp => serviceContent.includes(`${imp}.`));
931
- if (usedImports.length > 0) {
932
- const requireStatement = usedImports.length === 1
933
- ? `const { ${usedImports[0]} } = require('zod');`
934
- : `const { ${usedImports.join(', ')} } = require('zod');`;
935
- bundledPackages.add(requireStatement);
531
+ return {
532
+ name: skillMeta.name,
533
+ version: skillMeta.version,
534
+ description: skillMeta.description,
535
+ context: skillMeta.context,
536
+ tools: skillTools.map(tool => ({
537
+ name: tool.name,
538
+ description: tool.description || "",
539
+ inputSchema: tool.inputSchema || { type: "object" },
540
+ execute: compressCode(tool.executeCode || "")
541
+ }))
542
+ };
543
+ });
544
+ // Match skills from code with skills in YAML and create missing ones
545
+ const updatedSkillsArray = await ensureSkillsExistInYaml(skillsArray, config);
546
+ const deployData = {
547
+ skills: updatedSkillsArray
548
+ };
549
+ // Write legacy deploy.json to .lua directory
550
+ fs.writeFileSync(path.join(luaDir, "deploy.json"), JSON.stringify(deployData, null, 2));
551
+ // Write individual tool files to .lua directory (uncompressed)
552
+ for (const tool of tools) {
553
+ if (tool.executeCode) {
554
+ const toolFilePath = path.join(luaDir, `${tool.name}.js`);
555
+ fs.writeFileSync(toolFilePath, tool.executeCode);
556
+ }
557
+ }
558
+ }
559
+ function extractSkillsMetadata(indexFile) {
560
+ const skills = [];
561
+ // Find all LuaSkill constructor calls
562
+ indexFile.forEachDescendant((node) => {
563
+ if (Node.isNewExpression(node)) {
564
+ const expression = node.getExpression();
565
+ if (expression.getText() === 'LuaSkill') {
566
+ const args = node.getArguments();
567
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
568
+ const configObj = args[0];
569
+ let skillName = '';
570
+ let skillVersion = '';
571
+ let description = '';
572
+ let context = '';
573
+ let constructorTools = [];
574
+ // Extract properties
575
+ configObj.getProperties().forEach((prop) => {
576
+ if (Node.isPropertyAssignment(prop)) {
577
+ const name = prop.getName();
578
+ const value = prop.getInitializer();
579
+ if (name === 'name' && value) {
580
+ skillName = value.getText().replace(/['"]/g, '');
581
+ }
582
+ else if (name === 'version' && value) {
583
+ skillVersion = value.getText().replace(/['"]/g, '');
584
+ }
585
+ else if (name === 'description' && value) {
586
+ description = value.getText().replace(/['"]/g, '');
587
+ }
588
+ else if (name === 'context' && value) {
589
+ context = value.getText().replace(/['"]/g, '');
590
+ }
591
+ else if (name === 'tools' && value && Node.isArrayLiteralExpression(value)) {
592
+ // Extract tools from constructor array
593
+ const toolsArray = value;
594
+ constructorTools = toolsArray.getElements().map((element) => {
595
+ if (Node.isNewExpression(element)) {
596
+ return element.getExpression().getText();
597
+ }
598
+ return '';
599
+ }).filter(name => name);
936
600
  }
937
601
  }
938
- else if (defaultImport) {
939
- bundledPackages.add(`const ${defaultImport} = require('zod');`);
940
- }
941
- continue;
942
- }
943
- // Handle axios - bundle it properly
944
- if (packagePath === 'axios') {
945
- const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
946
- if (packageCode) {
947
- bundledPackages.add(packageCode);
948
- }
949
- continue;
950
- }
951
- // Bundle other external packages
952
- if (namedImports || defaultImport) {
953
- const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
954
- if (packageCode) {
955
- bundledPackages.add(packageCode);
956
- }
602
+ });
603
+ if (skillName) {
604
+ skills.push({
605
+ name: skillName,
606
+ version: skillVersion || '1.0.0',
607
+ description,
608
+ context,
609
+ constructorTools
610
+ });
957
611
  }
958
612
  }
959
- // Extract the service class with proper brace matching
960
- const classRegex = /class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{/g;
961
- let classMatch = classRegex.exec(serviceContent);
962
- if (classMatch) {
963
- const serviceClassName = classMatch[1];
964
- const startIndex = classMatch.index + classMatch[0].length - 1; // Position of opening brace
965
- // Find matching closing brace
966
- let braceCount = 1;
967
- let endIndex = startIndex + 1;
968
- while (endIndex < serviceContent.length && braceCount > 0) {
969
- if (serviceContent[endIndex] === '{')
970
- braceCount++;
971
- else if (serviceContent[endIndex] === '}')
972
- braceCount--;
973
- endIndex++;
613
+ }
614
+ }
615
+ });
616
+ return skills;
617
+ }
618
+ async function ensureSkillsExistInYaml(skillsArray, config) {
619
+ const updatedSkillsArray = [];
620
+ let yamlUpdated = false;
621
+ // Get existing skills from YAML
622
+ const existingSkills = config?.skills || [];
623
+ const existingSkillsMap = new Map();
624
+ // Create map of existing skills
625
+ existingSkills.forEach((skill) => {
626
+ existingSkillsMap.set(skill.name, skill);
627
+ });
628
+ // Process each detected skill
629
+ for (const skill of skillsArray) {
630
+ const existingSkill = existingSkillsMap.get(skill.name);
631
+ if (existingSkill && existingSkill.skillId && existingSkill.skillId !== '') {
632
+ // Skill exists with valid skillId - use it
633
+ // console.log(`Using existing skillId for ${skill.name}: ${existingSkill.skillId}`);
634
+ updatedSkillsArray.push({
635
+ ...skill,
636
+ skillId: existingSkill.skillId
637
+ });
638
+ }
639
+ else {
640
+ // Skill doesn't exist or missing skillId - create it via API
641
+ // console.log(`Creating new skill: ${skill.name}`);
642
+ try {
643
+ // Get API key and agent ID for skill creation
644
+ const apiKey = await loadApiKey();
645
+ if (!apiKey) {
646
+ throw new Error("No API key found. Run 'lua auth configure' first.");
647
+ }
648
+ const agentId = config?.agent?.agentId;
649
+ if (!agentId) {
650
+ throw new Error("No agent ID found in lua.skill.yaml. Run 'lua init' first.");
651
+ }
652
+ // Call create skill API
653
+ console.log(`Calling create skill API for: ${skill.name}`);
654
+ const skillPayload = {
655
+ name: skill.name,
656
+ description: skill.description || `A Lua skill for ${skill.name}`,
657
+ context: skill.context || ''
658
+ };
659
+ const result = await ApiService.Skill.createSkill(apiKey, agentId, skillPayload);
660
+ // console.log(`Create skill API response:`, result);
661
+ if (result.success && result.data && result.data.id) {
662
+ const newSkillId = result.data.id; // API returns 'id', not 'skillId'
663
+ // console.log(`✅ Created skill ${skill.name} with ID: ${newSkillId}`);
664
+ updatedSkillsArray.push({
665
+ ...skill,
666
+ skillId: newSkillId
667
+ });
668
+ // Update YAML with new skill
669
+ if (!existingSkill) {
670
+ existingSkills.push({
671
+ name: skill.name || '',
672
+ version: skill.version || '1.0.0',
673
+ skillId: newSkillId
674
+ });
675
+ yamlUpdated = true;
974
676
  }
975
- if (braceCount === 0) {
976
- const serviceClassBody = serviceContent.substring(startIndex + 1, endIndex - 1);
977
- // Clean up the class body (remove TypeScript types)
978
- const cleanClassBody = serviceClassBody
979
- .replace(/:\s*string/g, '')
980
- .replace(/:\s*number/g, '')
981
- .replace(/:\s*boolean/g, '')
982
- .replace(/:\s*any/g, '')
983
- .replace(/:\s*void/g, '')
984
- .replace(/:\s*Promise<[^>]+>/g, '')
985
- .replace(/:\s*Record<[^>]+>/g, '');
986
- // Create the service class
987
- const serviceClass = `class ${serviceClassName} {\n${cleanClassBody}\n}`;
988
- dependencies.push(serviceClass);
677
+ else {
678
+ // Update existing skill with new skillId
679
+ existingSkill.skillId = newSkillId;
680
+ yamlUpdated = true;
989
681
  }
990
682
  }
683
+ else {
684
+ console.error(`❌ Failed to create skill ${skill.name}:`, result.error);
685
+ throw new Error(result.error?.message || 'Failed to create skill - no ID returned from API');
686
+ }
991
687
  }
992
- continue;
993
- }
994
- // Bundle other external packages
995
- if (namedImports || defaultImport) {
996
- const packageCode = await bundlePackageCode(packagePath, namedImports, defaultImport);
997
- if (packageCode) {
998
- bundledPackages.add(packageCode);
688
+ catch (error) {
689
+ console.error(`❌ Failed to create skill ${skill.name}:`, error);
690
+ throw error; // Don't use temp IDs, let the process fail properly
999
691
  }
1000
692
  }
1001
693
  }
1002
- // 2. Extract class instantiation from constructor
1003
- const constructorMatch = toolContent.match(/constructor\s*\([^)]*\)\s*\{([\s\S]*?)\}/);
1004
- if (constructorMatch) {
1005
- const constructorBody = constructorMatch[1];
1006
- // Extract service instantiation
1007
- const serviceInstantiationMatch = constructorBody.match(/this\.(\w+)\s*=\s*new\s+(\w+)\([^)]*\);/);
1008
- if (serviceInstantiationMatch) {
1009
- const serviceProperty = serviceInstantiationMatch[1];
1010
- const serviceClass = serviceInstantiationMatch[2];
1011
- dependencies.push(`const ${serviceProperty} = new ${serviceClass}();`);
1012
- }
694
+ // Update YAML file if needed
695
+ if (yamlUpdated) {
696
+ await updateYamlWithSkills(existingSkills, config);
1013
697
  }
1014
- // 3. Create the self-contained execute function
1015
- const allDependencies = [...Array.from(bundledPackages), ...dependencies];
1016
- const dependencyCode = allDependencies.join('\n');
1017
- // Clean the execute body (remove TypeScript types)
1018
- const cleanExecuteBody = executeBody
1019
- .replace(/:\s*string/g, '')
1020
- .replace(/:\s*number/g, '')
1021
- .replace(/:\s*boolean/g, '')
1022
- .replace(/:\s*any/g, '')
1023
- .replace(/:\s*void/g, '')
1024
- .replace(/:\s*Promise<[^>]+>/g, '')
1025
- .replace(/:\s*Record<[^>]+>/g, '');
1026
- // Replace this.serviceProperty with the instantiated service
1027
- const finalExecuteBody = cleanExecuteBody.replace(/this\.(\w+)/g, '$1');
1028
- return `async (input) => {
1029
- ${dependencyCode ? dependencyCode + '\n' : ''}${finalExecuteBody}
1030
- }`;
698
+ return updatedSkillsArray;
1031
699
  }
1032
- async function extractSkillMetadata(indexContent) {
1033
- // Extract skillId and version from config file
1034
- let skillId = '';
1035
- let version = '';
1036
- const config = readSkillConfig();
1037
- if (config) {
1038
- skillId = config.skill?.skillId || '';
1039
- version = config.skill?.version || '';
1040
- }
1041
- // Extract description and context from LuaSkill constructor
1042
- let description = '';
1043
- let context = '';
1044
- // Look for LuaSkill constructor with object configuration
1045
- const luaSkillRegex = /new\s+LuaSkill\s*\(\s*\{([\s\S]*?)\}\s*\)/;
1046
- const match = luaSkillRegex.exec(indexContent);
1047
- if (match) {
1048
- const configContent = match[1];
1049
- // Extract description
1050
- const descriptionMatch = configContent.match(/description:\s*["']([^"']+)["']/);
1051
- if (descriptionMatch) {
1052
- description = descriptionMatch[1];
1053
- }
1054
- // Extract context
1055
- const contextMatch = configContent.match(/context:\s*["']([^"']+)["']/);
1056
- if (contextMatch) {
1057
- context = contextMatch[1];
1058
- }
1059
- }
1060
- return {
1061
- skillId,
1062
- version,
1063
- description,
1064
- context
700
+ async function updateYamlWithSkills(skills, config) {
701
+ // Clean skills array to ensure no undefined values
702
+ const cleanedSkills = skills.map(skill => ({
703
+ name: skill.name || '',
704
+ version: skill.version || '1.0.0',
705
+ skillId: skill.skillId || '' // Ensure skillId is never undefined
706
+ }));
707
+ // Update config with cleaned skills array
708
+ const updatedConfig = {
709
+ ...config,
710
+ skills: cleanedSkills
1065
711
  };
712
+ // Write updated YAML
713
+ const yamlPath = path.join(process.cwd(), 'lua.skill.yaml');
714
+ const yamlContent = yaml.dump(updatedConfig, {
715
+ indent: 2,
716
+ lineWidth: -1,
717
+ noRefs: true,
718
+ replacer: (key, value) => {
719
+ // Replace undefined values with empty strings
720
+ return value === undefined ? '' : value;
721
+ }
722
+ });
723
+ fs.writeFileSync(yamlPath, yamlContent);
724
+ console.log('Updated lua.skill.yaml with new skills');
1066
725
  }