lua-cli 1.3.2-alpha.2 → 1.3.2-alpha.3

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,661 @@
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 { Project, Node } from "ts-morph";
7
+ import { build } from "esbuild";
8
8
  // Compression utilities
9
9
  function compressCode(code) {
10
10
  const compressed = gzipSync(code);
11
11
  return compressed.toString('base64');
12
12
  }
13
- function decompressCode(compressedCode) {
14
- const buffer = Buffer.from(compressedCode, 'base64');
15
- return gunzipSync(buffer).toString('utf8');
16
- }
17
13
  export async function compileCommand() {
18
14
  return withErrorHandling(async () => {
19
15
  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
16
+ // Clean up old directories
17
+ const distDir = path.join(process.cwd(), "dist");
64
18
  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");
19
+ if (fs.existsSync(distDir)) {
20
+ fs.rmSync(distDir, { recursive: true, force: true });
21
+ }
22
+ if (fs.existsSync(luaDir)) {
23
+ fs.rmSync(luaDir, { recursive: true, force: true });
24
+ }
25
+ // Create directory structures
26
+ fs.mkdirSync(distDir, { recursive: true });
27
+ fs.mkdirSync(path.join(distDir, "tools"), { recursive: true });
28
+ fs.mkdirSync(luaDir, { recursive: true });
29
+ // Find index.ts file
30
+ const indexPath = findIndexFile();
31
+ // Use ts-morph to analyze the TypeScript project
32
+ const project = new Project({
33
+ tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
34
+ });
35
+ // Add the index file to the project
36
+ const indexFile = project.addSourceFileAtPath(indexPath);
37
+ // Detect tools from skill.addTools calls
38
+ const tools = await detectTools(indexFile, project);
39
+ writeProgress(`📦 Found ${tools.length} tools to bundle...`);
40
+ // Bundle each tool individually and extract execute code
41
+ for (const tool of tools) {
42
+ await bundleTool(tool, distDir);
43
+ await extractExecuteCode(tool, project);
44
+ }
45
+ // Bundle the main index file
46
+ await bundleMainIndex(indexPath, distDir);
47
+ // Create both deployment formats
48
+ await createDeploymentData(tools, distDir);
49
+ await createLegacyDeploymentData(tools, luaDir, indexFile);
50
+ writeSuccess(`✅ Skill compiled successfully - ${tools.length} tools bundled`);
77
51
  }, "compilation");
78
52
  }
79
- async function extractSkillInfo(indexContent, indexFilePath) {
53
+ function findIndexFile() {
54
+ // Check for index.ts in current directory
55
+ let indexPath = path.join(process.cwd(), "index.ts");
56
+ if (fs.existsSync(indexPath)) {
57
+ return indexPath;
58
+ }
59
+ // Check for index.ts in src directory
60
+ indexPath = path.join(process.cwd(), "src", "index.ts");
61
+ if (fs.existsSync(indexPath)) {
62
+ return indexPath;
63
+ }
64
+ throw new Error("index.ts not found in current directory or src/ directory");
65
+ }
66
+ async function detectTools(indexFile, project) {
80
67
  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;
97
- }
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]);
68
+ // Find all call expressions in the file
69
+ indexFile.forEachDescendant((node) => {
70
+ if (Node.isCallExpression(node)) {
71
+ const expression = node.getExpression();
72
+ // Check if this is skill.addTools or skill.addTool
73
+ if (Node.isPropertyAccessExpression(expression)) {
74
+ const object = expression.getExpression();
75
+ const property = expression.getName();
76
+ if (object.getText() === 'skill' && (property === 'addTools' || property === 'addTool')) {
77
+ const args = node.getArguments();
78
+ if (property === 'addTools' && args.length > 0) {
79
+ // Handle skill.addTools([...]) - array of tools
80
+ const arrayArg = args[0];
81
+ if (Node.isArrayLiteralExpression(arrayArg)) {
82
+ const elements = arrayArg.getElements();
83
+ for (const element of elements) {
84
+ if (Node.isNewExpression(element)) {
85
+ const toolInfo = extractToolFromNewExpressionSync(element, project);
86
+ if (toolInfo) {
87
+ tools.push(toolInfo);
88
+ }
89
+ }
90
+ }
91
+ }
103
92
  }
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);
93
+ else if (property === 'addTool' && args.length > 0) {
94
+ // Handle skill.addTool(new ToolClass()) - single tool
95
+ const arg = args[0];
96
+ if (Node.isNewExpression(arg)) {
97
+ const toolInfo = extractToolFromNewExpressionSync(arg, project);
98
+ if (toolInfo) {
99
+ tools.push(toolInfo);
100
+ }
110
101
  }
111
102
  }
112
103
  }
113
104
  }
114
- // Continue traversing
115
- ts.forEachChild(node, visit);
116
105
  }
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
- }
106
+ });
127
107
  return tools;
128
108
  }
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;
109
+ function extractToolFromNewExpressionSync(newExpr, project) {
110
+ try {
111
+ const expression = newExpr.getExpression();
112
+ const className = expression.getText();
113
+ // Find the import declaration for this class
114
+ const sourceFile = newExpr.getSourceFile();
115
+ const imports = sourceFile.getImportDeclarations();
116
+ for (const importDecl of imports) {
117
+ const namedImports = importDecl.getNamedImports();
118
+ const defaultImport = importDecl.getDefaultImport();
119
+ let toolFilePath = null;
120
+ // Check named imports
121
+ for (const namedImport of namedImports) {
122
+ if (namedImport.getName() === className) {
123
+ toolFilePath = resolveImportPath(importDecl.getModuleSpecifierValue(), sourceFile.getFilePath());
124
+ break;
174
125
  }
175
126
  }
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
- }
127
+ // Check default import
128
+ if (!toolFilePath && defaultImport && defaultImport.getText() === className) {
129
+ toolFilePath = resolveImportPath(importDecl.getModuleSpecifierValue(), sourceFile.getFilePath());
251
130
  }
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);
131
+ if (toolFilePath && fs.existsSync(toolFilePath)) {
132
+ // Extract tool metadata from the class file
133
+ try {
134
+ const toolSourceFile = project.addSourceFileAtPath(toolFilePath);
135
+ const classDecl = toolSourceFile.getClass(className);
136
+ if (classDecl) {
137
+ const nameProperty = classDecl.getProperty('name');
138
+ const descProperty = classDecl.getProperty('description');
139
+ let toolName = className.replace(/Tool$/, '').toLowerCase();
140
+ let description = '';
141
+ // Extract name from property if available
142
+ if (nameProperty && nameProperty.getInitializer()) {
143
+ const nameValue = nameProperty.getInitializer()?.getText();
144
+ if (nameValue) {
145
+ toolName = nameValue.replace(/['"]/g, '');
146
+ }
147
+ }
148
+ // Extract description from property if available
149
+ if (descProperty && descProperty.getInitializer()) {
150
+ const descValue = descProperty.getInitializer()?.getText();
151
+ if (descValue) {
152
+ description = descValue.replace(/['"]/g, '');
153
+ }
295
154
  }
155
+ return {
156
+ name: toolName,
157
+ className,
158
+ filePath: toolFilePath,
159
+ description
160
+ };
296
161
  }
297
- return { type: 'string', enum: enumValues };
298
162
  }
299
- return { type: 'string' };
300
- default:
301
- return { type: 'string' };
163
+ catch (fileError) {
164
+ console.warn(`Warning: Could not load tool file ${toolFilePath}:`, fileError);
165
+ }
166
+ }
302
167
  }
168
+ return null;
303
169
  }
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;
170
+ catch (error) {
171
+ console.warn(`Warning: Could not extract tool info for ${newExpr.getText()}:`, error);
172
+ return null;
315
173
  }
316
- return { type: 'string' };
317
174
  }
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;
175
+ function resolveImportPath(moduleSpecifier, currentFilePath) {
176
+ if (moduleSpecifier.startsWith('./') || moduleSpecifier.startsWith('../')) {
177
+ // Relative import - resolve relative to current file
178
+ const currentDir = path.dirname(currentFilePath);
179
+ return path.resolve(currentDir, moduleSpecifier + '.ts');
325
180
  }
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);
181
+ else {
182
+ // Absolute import - assume it's in the project
183
+ return path.resolve(process.cwd(), 'src', moduleSpecifier + '.ts');
342
184
  }
343
- findSchema(sourceFile);
344
- return schema;
345
185
  }
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
- }
186
+ async function bundleTool(tool, distDir) {
187
+ writeProgress(`📦 Bundling ${tool.className}...`);
188
+ try {
189
+ const outputPath = path.join(distDir, 'tools', `${tool.className}.js`);
190
+ await build({
191
+ entryPoints: [tool.filePath],
192
+ bundle: true,
193
+ format: 'cjs',
194
+ platform: 'node',
195
+ target: 'node16',
196
+ outfile: outputPath,
197
+ external: ['lua-cli/skill', 'lua-cli/user-data-api', 'zod'], // Exclude lua-cli modules and zod - injected into VM
198
+ minify: true, // Minify for smaller file sizes
199
+ sourcemap: false,
200
+ resolveExtensions: ['.ts', '.js', '.json'],
201
+ define: {
202
+ 'process.env.NODE_ENV': '"production"'
203
+ },
204
+ // Ensure all other dependencies are bundled
205
+ packages: 'bundle',
206
+ // Handle different module formats
207
+ mainFields: ['main', 'module'],
208
+ conditions: ['node']
209
+ });
210
+ // Add VM-compatible wrapper
211
+ await wrapToolForVM(outputPath, tool);
212
+ }
213
+ catch (error) {
214
+ console.warn(`Warning: Failed to bundle ${tool.className}:`, error);
356
215
  }
357
- return envVars;
358
216
  }
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);
388
- }
389
- }
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
- }
217
+ async function wrapToolForVM(outputPath, tool) {
218
+ const bundledCode = fs.readFileSync(outputPath, 'utf8');
219
+ // Create a wrapper that's compatible with the existing sandbox.ts VM system
220
+ // The sandbox expects: const executeFunction = ${toolCode}; module.exports = async (input) => { return await executeFunction(input); };
221
+ const wrappedCode = `async (input) => {
222
+ // Mock lua-cli/skill module for external dependencies
223
+ const luaCliSkill = {
224
+ env: function(key) {
225
+ if (typeof env !== 'undefined') {
226
+ return env(key);
227
+ }
228
+ return process.env[key] || '';
402
229
  }
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
- }
230
+ };
231
+
232
+ // Mock lua-cli/user-data-api module
233
+ const luaCliUserDataApi = {
234
+ user: typeof user !== 'undefined' ? user : {
235
+ data: {
236
+ get: async () => ({}),
237
+ update: async (data) => data,
238
+ create: async (data) => data
239
+ }
450
240
  }
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
- }
241
+ };
242
+
243
+ // Mock zod module
244
+ const zodModule = (() => {
245
+ try {
246
+ if (typeof require !== 'undefined') {
247
+ return require('zod');
248
+ }
249
+ } catch (e) {
250
+ // Fallback zod implementation
460
251
  }
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
- }
252
+ return {
253
+ z: {
254
+ object: (schema) => ({
255
+ parse: (data) => data,
256
+ safeParse: (data) => ({ success: true, data })
257
+ }),
258
+ string: () => ({
259
+ parse: (data) => String(data),
260
+ safeParse: (data) => ({ success: true, data: String(data) })
261
+ }),
262
+ number: () => ({
263
+ parse: (data) => Number(data),
264
+ safeParse: (data) => ({ success: true, data: Number(data) })
265
+ }),
266
+ boolean: () => ({
267
+ parse: (data) => Boolean(data),
268
+ safeParse: (data) => ({ success: true, data: Boolean(data) })
269
+ })
270
+ }
271
+ };
272
+ })();
273
+
274
+ // Override require for external modules during execution
275
+ const originalRequire = require;
276
+ require = function(id) {
277
+ if (id === 'lua-cli/skill') {
278
+ return luaCliSkill;
480
279
  }
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
- }
280
+ if (id === 'lua-cli/user-data-api') {
281
+ return luaCliUserDataApi;
495
282
  }
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()}
283
+ if (id === 'zod') {
284
+ return zodModule;
285
+ }
286
+ return originalRequire(id);
287
+ };
288
+
289
+ // Execute the bundled tool code
290
+ ${bundledCode}
291
+
292
+ // Restore original require
293
+ require = originalRequire;
294
+
295
+ // Get the tool class from exports
296
+ const ToolClass = module.exports.default || module.exports.${tool.className} || module.exports;
297
+
298
+ // Create and execute the tool
299
+ const toolInstance = new ToolClass();
300
+ return await toolInstance.execute(input);
528
301
  }`;
529
- return selfContainedExecute;
302
+ fs.writeFileSync(outputPath, wrappedCode);
530
303
  }
531
- async function bundlePackageCode(packagePath, namedImports, defaultImport) {
304
+ async function bundleMainIndex(indexPath, distDir) {
305
+ writeProgress("📦 Bundling main index...");
532
306
  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],
307
+ await build({
308
+ entryPoints: [indexPath],
561
309
  bundle: true,
562
- format: 'cjs', // CommonJS format
310
+ format: 'cjs',
563
311
  platform: 'node',
564
312
  target: 'node16',
565
- outfile: outputFile,
566
- external: [], // Bundle everything
567
- minify: false, // Keep readable for debugging
313
+ outfile: path.join(distDir, 'index.js'),
314
+ external: ['lua-cli/skill', 'lua-cli/user-data-api', 'zod'], // Exclude lua-cli modules and zod
315
+ minify: true, // Minify for smaller file sizes
568
316
  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(),
317
+ resolveExtensions: ['.ts', '.js', '.json'],
318
+ define: {
319
+ 'process.env.NODE_ENV': '"production"'
320
+ },
321
+ packages: 'bundle',
322
+ mainFields: ['main', 'module'],
323
+ conditions: ['node']
579
324
  });
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;
325
+ }
326
+ catch (error) {
327
+ console.warn("Warning: Failed to bundle main index:", error);
328
+ }
329
+ }
330
+ async function extractExecuteCode(tool, project) {
331
+ try {
332
+ const toolSourceFile = project.getSourceFile(tool.filePath);
333
+ if (!toolSourceFile) {
334
+ console.warn(`Warning: Could not find source file for ${tool.className}`);
335
+ return;
588
336
  }
589
- const bundledContent = fs.readFileSync(outputFile, 'utf8');
590
- // Clean up temporary files
591
- try {
592
- fs.unlinkSync(entryFile);
593
- fs.unlinkSync(outputFile);
337
+ const classDecl = toolSourceFile.getClass(tool.className);
338
+ if (!classDecl) {
339
+ console.warn(`Warning: Could not find class ${tool.className} in ${tool.filePath}`);
340
+ return;
594
341
  }
595
- catch (cleanupError) {
596
- // Ignore cleanup errors
342
+ // Extract the execute method
343
+ const executeMethod = classDecl.getMethod('execute');
344
+ if (!executeMethod) {
345
+ console.warn(`Warning: Could not find execute method in ${tool.className}`);
346
+ return;
597
347
  }
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`;
348
+ // Get the execute method body
349
+ const executeBody = executeMethod.getBodyText();
350
+ if (!executeBody) {
351
+ console.warn(`Warning: Execute method has no body in ${tool.className}`);
352
+ return;
602
353
  }
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`;
354
+ // Read the bundled tool file to get the self-contained code
355
+ const bundledPath = path.join(process.cwd(), 'dist', 'tools', `${tool.className}.js`);
356
+ if (fs.existsSync(bundledPath)) {
357
+ const bundledCode = fs.readFileSync(bundledPath, 'utf8');
358
+ // Extract just the tool execution function from the bundled code
359
+ // The bundled code contains the VM wrapper, we need to extract the core functionality
360
+ const executeFunction = createExecuteFunction(bundledCode, tool);
361
+ tool.executeCode = executeFunction;
362
+ }
363
+ // Extract input schema from zod definition
364
+ const inputSchemaProperty = classDecl.getProperty('inputSchema');
365
+ if (inputSchemaProperty && inputSchemaProperty.getInitializer()) {
366
+ const initializer = inputSchemaProperty.getInitializer();
367
+ if (initializer) {
368
+ // Parse the zod schema to extract JSON schema
369
+ tool.inputSchema = parseZodSchemaToJsonSchema(initializer.getText());
370
+ }
607
371
  }
608
- else {
609
- finalCode = `(function() {\n${bundledContent}\n})();\n`;
372
+ if (!tool.inputSchema) {
373
+ tool.inputSchema = { type: "object" };
610
374
  }
611
- return finalCode;
612
375
  }
613
376
  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;
377
+ console.warn(`Warning: Could not extract execute code for ${tool.className}:`, error);
620
378
  }
621
379
  }
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
635
- }
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;
668
- }
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
- },
679
-
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;
694
- }
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
- },
705
-
706
- delete: async (url, config = {}) => {
707
- const response = await fetch(url, {
708
- method: 'DELETE',
709
- headers: {
710
- 'Content-Type': 'application/json',
711
- ...config.headers
712
- }
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;
719
- }
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
728
- };
729
- },
730
-
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;
745
- }
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
- `;
380
+ function createExecuteFunction(bundledCode, tool) {
381
+ // The bundled code is already wrapped for VM execution by wrapToolForVM
382
+ // Just return it directly since it's already an async function
383
+ return bundledCode;
758
384
  }
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
- }
385
+ function parseZodSchemaToJsonSchema(zodSchemaText) {
386
+ try {
387
+ // Use ts-morph to properly parse the Zod schema
388
+ const tempProject = new Project();
389
+ const tempFile = tempProject.createSourceFile('temp.ts', `
390
+ import { z } from 'zod';
391
+ const schema = ${zodSchemaText};
392
+ `);
393
+ // Find the schema variable declaration
394
+ const schemaDeclaration = tempFile.getVariableDeclaration('schema');
395
+ if (schemaDeclaration && schemaDeclaration.getInitializer()) {
396
+ const initializer = schemaDeclaration.getInitializer();
397
+ if (initializer) {
398
+ return parseZodASTToJsonSchema(initializer);
779
399
  }
780
400
  }
781
- ts.forEachChild(node, findImport);
782
- }
783
- findImport(sourceFile);
784
- if (!importPath) {
785
- console.warn(`Warning: Could not find import for class ${className}`);
786
- return null;
401
+ // Fallback to direct parsing if AST approach fails
402
+ return parseZodTextToJsonSchema(zodSchemaText);
787
403
  }
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');
794
- }
795
- else {
796
- // Absolute path or just filename
797
- toolFilePath = path.join(process.cwd(), importPath + '.ts');
798
- }
799
- if (!fs.existsSync(toolFilePath)) {
800
- console.warn(`Warning: Tool file not found: ${toolFilePath}`);
801
- return null;
404
+ catch (error) {
405
+ console.warn('Warning: Could not parse Zod schema with AST, falling back to text parsing:', error);
406
+ return parseZodTextToJsonSchema(zodSchemaText);
802
407
  }
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;
408
+ }
409
+ function parseZodASTToJsonSchema(node) {
410
+ try {
411
+ // Handle z.object({ ... })
412
+ if (Node.isCallExpression(node)) {
413
+ const expression = node.getExpression();
414
+ if (Node.isPropertyAccessExpression(expression)) {
415
+ const object = expression.getExpression();
416
+ const property = expression.getName();
417
+ if (object.getText() === 'z' && property === 'object') {
418
+ const args = node.getArguments();
419
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
420
+ const objectLiteral = args[0];
421
+ const properties = {};
422
+ const required = [];
423
+ objectLiteral.getProperties().forEach((prop) => {
424
+ if (Node.isPropertyAssignment(prop)) {
425
+ const name = prop.getName();
426
+ const value = prop.getInitializer();
427
+ if (value) {
428
+ const propSchema = parseZodASTToJsonSchema(value);
429
+ properties[name] = propSchema;
430
+ // Check if it's required (not optional)
431
+ if (!isOptionalZodType(value)) {
432
+ required.push(name);
433
+ }
434
+ }
435
+ }
436
+ });
437
+ return {
438
+ type: 'object',
439
+ properties,
440
+ required
441
+ };
824
442
  }
825
- // Extract inputSchema property
826
- if (propertyName === 'inputSchema' && member.initializer) {
827
- inputSchema = extractZodSchemaFromAST(member.initializer);
443
+ }
444
+ // Handle other z.* types
445
+ if (object.getText() === 'z') {
446
+ switch (property) {
447
+ case 'string':
448
+ return { type: 'string' };
449
+ case 'number':
450
+ return { type: 'number' };
451
+ case 'boolean':
452
+ return { type: 'boolean' };
453
+ case 'any':
454
+ return { type: 'object' };
455
+ case 'array':
456
+ const arrayArgs = node.getArguments();
457
+ if (arrayArgs.length > 0) {
458
+ const itemSchema = parseZodASTToJsonSchema(arrayArgs[0]);
459
+ return { type: 'array', items: itemSchema };
460
+ }
461
+ return { type: 'array' };
462
+ case 'enum':
463
+ const enumArgs = node.getArguments();
464
+ if (enumArgs.length > 0 && Node.isArrayLiteralExpression(enumArgs[0])) {
465
+ const enumValues = enumArgs[0].getElements().map((element) => {
466
+ if (Node.isStringLiteral(element)) {
467
+ return element.getLiteralValue();
468
+ }
469
+ return element.getText().replace(/['"]/g, '');
470
+ });
471
+ return { type: 'string', enum: enumValues };
472
+ }
473
+ return { type: 'string' };
828
474
  }
829
475
  }
830
- // Extract execute method
831
- if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name) && member.name.text === 'execute') {
832
- executeMethod = member;
476
+ }
477
+ // Handle method chaining like z.string().optional()
478
+ if (Node.isPropertyAccessExpression(expression)) {
479
+ const baseExpression = expression.getExpression();
480
+ const method = expression.getName();
481
+ if (method === 'optional' && Node.isCallExpression(baseExpression)) {
482
+ // This is an optional field, parse the base type
483
+ return parseZodASTToJsonSchema(baseExpression);
833
484
  }
834
485
  }
835
486
  }
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;
487
+ // Fallback
488
+ return { type: 'object' };
842
489
  }
843
- if (!inputSchema) {
844
- console.warn(`Warning: Could not find input schema in ${className}`);
845
- return null;
846
- }
847
- if (!executeMethod) {
848
- console.warn(`Warning: Could not find execute method in ${className}`);
849
- return null;
850
- }
851
- // Extract execute method body
852
- if (!executeMethod.body) {
853
- console.warn(`Warning: Execute method has no body in ${className}`);
854
- return null;
490
+ catch (error) {
491
+ console.warn('Warning: Could not parse Zod AST node:', error);
492
+ return { type: 'object' };
855
493
  }
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
867
- };
868
494
  }
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;
495
+ function isOptionalZodType(node) {
496
+ // Check if this is a method call ending with .optional()
497
+ if (Node.isCallExpression(node)) {
498
+ const expression = node.getExpression();
499
+ if (Node.isPropertyAccessExpression(expression)) {
500
+ const method = expression.getName();
501
+ return method === 'optional';
882
502
  }
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);
503
+ }
504
+ return false;
505
+ }
506
+ function parseZodTextToJsonSchema(zodSchemaText) {
507
+ try {
508
+ // Fallback text-based parsing for when AST parsing fails
509
+ if (zodSchemaText.includes('z.object({')) {
510
+ const properties = {};
511
+ const required = [];
512
+ // Extract object properties - enhanced pattern matching
513
+ const propertyPattern = /(\w+):\s*z\.(\w+)\(\)/g;
514
+ let match;
515
+ while ((match = propertyPattern.exec(zodSchemaText)) !== null) {
516
+ const [, propName, zodType] = match;
517
+ switch (zodType) {
518
+ case 'string':
519
+ properties[propName] = { type: 'string' };
520
+ break;
521
+ case 'number':
522
+ properties[propName] = { type: 'number' };
523
+ break;
524
+ case 'boolean':
525
+ properties[propName] = { type: 'boolean' };
526
+ break;
527
+ case 'any':
528
+ properties[propName] = { type: 'object' };
529
+ break;
530
+ default:
531
+ properties[propName] = { type: 'string' };
532
+ }
533
+ // Check if it's optional
534
+ if (!zodSchemaText.includes(`${propName}: z.${zodType}().optional()`)) {
535
+ required.push(propName);
893
536
  }
894
537
  }
895
- else if (defaultImport) {
896
- bundledPackages.add(`const ${defaultImport} = require('zod');`);
897
- }
898
- continue;
899
- }
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);
905
- }
906
- continue;
907
- }
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);
936
- }
937
- }
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
- }
957
- }
538
+ // Handle arrays
539
+ const arrayPattern = /(\w+):\s*z\.array\(z\.(\w+)\(\)\)/g;
540
+ while ((match = arrayPattern.exec(zodSchemaText)) !== null) {
541
+ const [, propName, itemType] = match;
542
+ let itemSchema = { type: 'string' };
543
+ switch (itemType) {
544
+ case 'string':
545
+ itemSchema = { type: 'string' };
546
+ break;
547
+ case 'number':
548
+ itemSchema = { type: 'number' };
549
+ break;
550
+ case 'boolean':
551
+ itemSchema = { type: 'boolean' };
552
+ break;
958
553
  }
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++;
974
- }
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);
989
- }
554
+ properties[propName] = { type: 'array', items: itemSchema };
555
+ if (!zodSchemaText.includes(`${propName}: z.array(z.${itemType}()).optional()`)) {
556
+ required.push(propName);
990
557
  }
991
558
  }
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);
999
- }
559
+ return {
560
+ type: 'object',
561
+ properties,
562
+ required
563
+ };
1000
564
  }
565
+ // Handle simple types
566
+ if (zodSchemaText.includes('z.string()'))
567
+ return { type: 'string' };
568
+ if (zodSchemaText.includes('z.number()'))
569
+ return { type: 'number' };
570
+ if (zodSchemaText.includes('z.boolean()'))
571
+ return { type: 'boolean' };
572
+ if (zodSchemaText.includes('z.array('))
573
+ return { type: 'array' };
574
+ // Fallback
575
+ return { type: 'object' };
1001
576
  }
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
- }
577
+ catch (error) {
578
+ console.warn('Warning: Could not parse Zod schema text:', error);
579
+ return { type: 'object' };
1013
580
  }
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
- }`;
1031
581
  }
1032
- async function extractSkillMetadata(indexContent) {
1033
- // Extract skillId and version from config file
1034
- let skillId = '';
1035
- let version = '';
582
+ async function createDeploymentData(tools, distDir) {
583
+ const config = readSkillConfig();
584
+ const packageJsonPath = path.join(process.cwd(), "package.json");
585
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
586
+ const deploymentData = {
587
+ name: config?.skill?.name || packageJson.name || "lua-skill",
588
+ version: config?.skill?.version || packageJson.version || "1.0.0",
589
+ skillId: config?.skill?.skillId || "",
590
+ description: config?.skill?.description || packageJson.description || "",
591
+ context: config?.skill?.context || "",
592
+ tools: tools.map(tool => ({
593
+ name: tool.name,
594
+ className: tool.className,
595
+ description: tool.description || "",
596
+ filePath: `tools/${tool.className}.js`
597
+ }))
598
+ };
599
+ fs.writeFileSync(path.join(distDir, 'deployment.json'), JSON.stringify(deploymentData, null, 2));
600
+ }
601
+ async function createLegacyDeploymentData(tools, luaDir, indexFile) {
1036
602
  const config = readSkillConfig();
1037
- if (config) {
1038
- skillId = config.skill?.skillId || '';
1039
- version = config.skill?.version || '';
603
+ const packageJsonPath = path.join(process.cwd(), "package.json");
604
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
605
+ // Extract skill metadata from index.ts
606
+ const skillMetadata = extractSkillMetadata(indexFile);
607
+ const deployData = {
608
+ version: config?.skill?.version || packageJson.version || "1.0.0",
609
+ name: config?.skill?.name || packageJson.name || "lua-skill",
610
+ skillsName: config?.skill?.name || packageJson.name || "lua-skill",
611
+ skillId: config?.skill?.skillId || skillMetadata.skillId || "",
612
+ description: config?.skill?.description || skillMetadata.description || "",
613
+ context: config?.skill?.context || skillMetadata.context || "",
614
+ tools: tools.map(tool => ({
615
+ name: tool.name,
616
+ description: tool.description || "",
617
+ inputSchema: tool.inputSchema || { type: "object" },
618
+ execute: compressCode(tool.executeCode || "")
619
+ }))
620
+ };
621
+ // Write legacy deploy.json to .lua directory
622
+ fs.writeFileSync(path.join(luaDir, "deploy.json"), JSON.stringify(deployData, null, 2));
623
+ // Write individual tool files to .lua directory (uncompressed)
624
+ for (const tool of tools) {
625
+ if (tool.executeCode) {
626
+ const toolFilePath = path.join(luaDir, `${tool.name}.js`);
627
+ fs.writeFileSync(toolFilePath, tool.executeCode);
628
+ }
1040
629
  }
1041
- // Extract description and context from LuaSkill constructor
630
+ }
631
+ function extractSkillMetadata(indexFile) {
632
+ let skillId = '';
1042
633
  let description = '';
1043
634
  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];
635
+ // Find LuaSkill constructor call
636
+ indexFile.forEachDescendant((node) => {
637
+ if (Node.isNewExpression(node)) {
638
+ const expression = node.getExpression();
639
+ if (expression.getText() === 'LuaSkill') {
640
+ const args = node.getArguments();
641
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
642
+ const configObj = args[0];
643
+ // Extract properties
644
+ configObj.getProperties().forEach((prop) => {
645
+ if (Node.isPropertyAssignment(prop)) {
646
+ const name = prop.getName();
647
+ const value = prop.getInitializer()?.getText();
648
+ if (name === 'description' && value) {
649
+ description = value.replace(/['"]/g, '');
650
+ }
651
+ else if (name === 'context' && value) {
652
+ context = value.replace(/['"]/g, '');
653
+ }
654
+ }
655
+ });
656
+ }
657
+ }
1058
658
  }
1059
- }
1060
- return {
1061
- skillId,
1062
- version,
1063
- description,
1064
- context
1065
- };
659
+ });
660
+ return { skillId, description, context };
1066
661
  }