lua-cli 1.3.2-alpha.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.2-alpha.3] - 2025-09-25
9
+
10
+ ### 🚀 Major Rewrite
11
+ - **Complete compiler rewrite** using TypeScript AST (ts-morph) instead of regex
12
+ - **Self-contained tool bundling** with esbuild and minification
13
+ - **85% file size reduction** (4MB → 612KB total bundle size)
14
+ - **Zero regex usage** - pure AST-based parsing for reliability
15
+
16
+ ### ✨ New Features
17
+ - **Enhanced Zod schema parsing** for nested objects and arrays
18
+ - **Comprehensive polyfills** for browser/Node.js APIs (AbortController, FormData, TextEncoder, crypto, etc.)
19
+ - **Optimized bundling** excludes lua-cli modules (injected by VM) for smaller sizes
20
+ - **Dual output format** - both new (dist/) and legacy (.lua/) formats
21
+
22
+ ### 🔧 Improvements
23
+ - **Proper environment variable injection** from lua.skill.yaml into VM
24
+ - **Minified output** for production deployment
25
+ - **Better error handling** with proper TypeScript AST validation
26
+ - **Full backward compatibility** with existing deployment workflow
27
+
28
+ ### đŸ“Ļ Bundle Size Optimizations
29
+ - Simple tools (Weather, UserData): 545KB → 4KB (99% reduction)
30
+ - HTTP tools (CreatePost): 863KB → 232KB (73% reduction)
31
+ - AI tools (SearchMovies): 1.4MB → 364KB (74% reduction)
32
+
8
33
  ## [1.3.0-alpha.1] - 2024-12-19
9
34
 
10
35
  ### Added
package/README.md CHANGED
@@ -195,7 +195,7 @@ This command will:
195
195
  - Available at `http://localhost:3000` (opens automatically)
196
196
  - **Live Log Panel**:
197
197
  - Real-time log feed showing execution details
198
- - WebSocket connection to `wss://api.lua.dev/feed`
198
+ - WebSocket connection to `wss://api.heylua.ai/feed`
199
199
  - Console-style interface with color-coded log levels
200
200
  - Shows tool calls, errors, metrics, and execution metadata
201
201
  - Displays detailed information in expandable sections
@@ -498,7 +498,7 @@ For more details, see [USER_DATA_API.md](./USER_DATA_API.md).
498
498
 
499
499
  For support and questions:
500
500
  - Create an issue on [GitHub](https://github.com/lua-ai-global/lua-cli/issues)
501
- - Contact: stefan@lua.dev
501
+ - Contact: stefan@heylua.ai
502
502
 
503
503
  ## Changelog
504
504
 
@@ -5,7 +5,7 @@ export async function agentsCommand() {
5
5
  return withErrorHandling(async () => {
6
6
  const apiKey = await loadApiKey();
7
7
  if (!apiKey) {
8
- console.error("❌ No API key found. Run `lua configure` first.");
8
+ console.error("❌ No API key found. Run `lua auth configure` first.");
9
9
  process.exit(1);
10
10
  }
11
11
  const response = await ApiService.Agent.getOrganizations(apiKey);
@@ -1,11 +1,11 @@
1
1
  import inquirer from "inquirer";
2
2
  import { loadApiKey } from "../services/auth.js";
3
- import { withErrorHandling, clearPromptLines, writeProgress, writeSuccess } from "../utils/cli.js";
3
+ import { withErrorHandling, writeProgress, writeSuccess } from "../utils/cli.js";
4
4
  export async function apiKeyCommand() {
5
5
  return withErrorHandling(async () => {
6
6
  const apiKey = await loadApiKey();
7
7
  if (!apiKey) {
8
- writeProgress("â„šī¸ No API key found. Run `lua configure` first.");
8
+ writeProgress("â„šī¸ No API key found. Run `lua auth configure` first.");
9
9
  return;
10
10
  }
11
11
  const { confirm } = await inquirer.prompt([
@@ -16,8 +16,6 @@ export async function apiKeyCommand() {
16
16
  default: false
17
17
  }
18
18
  ]);
19
- // Clear the confirmation prompt lines
20
- clearPromptLines(2);
21
19
  if (confirm) {
22
20
  writeSuccess("🔑 Your API key:");
23
21
  console.log(apiKey);
@@ -3,8 +3,11 @@ import path from "path";
3
3
  import { gzipSync } from "zlib";
4
4
  import { withErrorHandling, writeProgress, writeSuccess } from "../utils/cli.js";
5
5
  import { readSkillConfig } from '../utils/files.js';
6
+ import { loadApiKey } from '../services/auth.js';
7
+ import { ApiService } from '../services/api.js';
6
8
  import { Project, Node } from "ts-morph";
7
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);
@@ -65,7 +68,36 @@ function findIndexFile() {
65
68
  }
66
69
  async function detectTools(indexFile, project) {
67
70
  const tools = [];
68
- // Find all call expressions in the file
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
+ });
96
+ }
97
+ }
98
+ }
99
+ });
100
+ // Find all call expressions in the file (addTools method calls)
69
101
  indexFile.forEachDescendant((node) => {
70
102
  if (Node.isCallExpression(node)) {
71
103
  const expression = node.getExpression();
@@ -73,7 +105,7 @@ async function detectTools(indexFile, project) {
73
105
  if (Node.isPropertyAccessExpression(expression)) {
74
106
  const object = expression.getExpression();
75
107
  const property = expression.getName();
76
- if (object.getText() === 'skill' && (property === 'addTools' || property === 'addTool')) {
108
+ if (property === 'addTools' || property === 'addTool') {
77
109
  const args = node.getArguments();
78
110
  if (property === 'addTools' && args.length > 0) {
79
111
  // Handle skill.addTools([...]) - array of tools
@@ -152,12 +184,13 @@ function extractToolFromNewExpressionSync(newExpr, project) {
152
184
  description = descValue.replace(/['"]/g, '');
153
185
  }
154
186
  }
155
- return {
187
+ const toolInfo = {
156
188
  name: toolName,
157
189
  className,
158
190
  filePath: toolFilePath,
159
191
  description
160
192
  };
193
+ return toolInfo;
161
194
  }
162
195
  }
163
196
  catch (fileError) {
@@ -360,13 +393,20 @@ async function extractExecuteCode(tool, project) {
360
393
  const executeFunction = createExecuteFunction(bundledCode, tool);
361
394
  tool.executeCode = executeFunction;
362
395
  }
363
- // Extract input schema from zod definition
396
+ // Extract input schema from zod definition using proper library
364
397
  const inputSchemaProperty = classDecl.getProperty('inputSchema');
365
398
  if (inputSchemaProperty && inputSchemaProperty.getInitializer()) {
366
399
  const initializer = inputSchemaProperty.getInitializer();
367
400
  if (initializer) {
368
- // Parse the zod schema to extract JSON schema
369
- tool.inputSchema = parseZodSchemaToJsonSchema(initializer.getText());
401
+ try {
402
+ // Evaluate the Zod schema to get the actual schema object
403
+ const zodSchemaCode = initializer.getText();
404
+ tool.inputSchema = await evaluateZodSchemaToJsonSchema(zodSchemaCode);
405
+ }
406
+ catch (error) {
407
+ console.warn(`Warning: Could not parse Zod schema for ${tool.className}:`, error);
408
+ tool.inputSchema = { type: "object" };
409
+ }
370
410
  }
371
411
  }
372
412
  if (!tool.inputSchema) {
@@ -382,200 +422,37 @@ function createExecuteFunction(bundledCode, tool) {
382
422
  // Just return it directly since it's already an async function
383
423
  return bundledCode;
384
424
  }
385
- function parseZodSchemaToJsonSchema(zodSchemaText) {
425
+ async function evaluateZodSchemaToJsonSchema(zodSchemaCode) {
386
426
  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);
399
- }
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;
400
439
  }
401
- // Fallback to direct parsing if AST approach fails
402
- return parseZodTextToJsonSchema(zodSchemaText);
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;
403
443
  }
404
444
  catch (error) {
405
- console.warn('Warning: Could not parse Zod schema with AST, falling back to text parsing:', error);
406
- return parseZodTextToJsonSchema(zodSchemaText);
407
- }
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
- };
442
- }
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' };
474
- }
475
- }
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);
484
- }
485
- }
486
- }
487
- // Fallback
488
- return { type: 'object' };
489
- }
490
- catch (error) {
491
- console.warn('Warning: Could not parse Zod AST node:', error);
492
- return { type: 'object' };
493
- }
494
- }
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';
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' };
502
449
  }
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);
536
- }
537
- }
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;
553
- }
554
- properties[propName] = { type: 'array', items: itemSchema };
555
- if (!zodSchemaText.includes(`${propName}: z.array(z.${itemType}()).optional()`)) {
556
- required.push(propName);
557
- }
558
- }
559
- return {
560
- type: 'object',
561
- properties,
562
- required
563
- };
564
- }
565
- // Handle simple types
566
- if (zodSchemaText.includes('z.string()'))
450
+ if (zodSchemaCode.includes('z.string()'))
567
451
  return { type: 'string' };
568
- if (zodSchemaText.includes('z.number()'))
452
+ if (zodSchemaCode.includes('z.number()'))
569
453
  return { type: 'number' };
570
- if (zodSchemaText.includes('z.boolean()'))
454
+ if (zodSchemaCode.includes('z.boolean()'))
571
455
  return { type: 'boolean' };
572
- if (zodSchemaText.includes('z.array('))
573
- return { type: 'array' };
574
- // Fallback
575
- return { type: 'object' };
576
- }
577
- catch (error) {
578
- console.warn('Warning: Could not parse Zod schema text:', error);
579
456
  return { type: 'object' };
580
457
  }
581
458
  }
@@ -602,21 +479,72 @@ async function createLegacyDeploymentData(tools, luaDir, indexFile) {
602
479
  const config = readSkillConfig();
603
480
  const packageJsonPath = path.join(process.cwd(), "package.json");
604
481
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
605
- // Extract skill metadata from index.ts
606
- const skillMetadata = extractSkillMetadata(indexFile);
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);
504
+ }
505
+ }
506
+ }
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
+ }
529
+ }
530
+ }
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);
607
546
  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
- }))
547
+ skills: updatedSkillsArray
620
548
  };
621
549
  // Write legacy deploy.json to .lua directory
622
550
  fs.writeFileSync(path.join(luaDir, "deploy.json"), JSON.stringify(deployData, null, 2));
@@ -628,11 +556,9 @@ async function createLegacyDeploymentData(tools, luaDir, indexFile) {
628
556
  }
629
557
  }
630
558
  }
631
- function extractSkillMetadata(indexFile) {
632
- let skillId = '';
633
- let description = '';
634
- let context = '';
635
- // Find LuaSkill constructor call
559
+ function extractSkillsMetadata(indexFile) {
560
+ const skills = [];
561
+ // Find all LuaSkill constructor calls
636
562
  indexFile.forEachDescendant((node) => {
637
563
  if (Node.isNewExpression(node)) {
638
564
  const expression = node.getExpression();
@@ -640,22 +566,160 @@ function extractSkillMetadata(indexFile) {
640
566
  const args = node.getArguments();
641
567
  if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
642
568
  const configObj = args[0];
569
+ let skillName = '';
570
+ let skillVersion = '';
571
+ let description = '';
572
+ let context = '';
573
+ let constructorTools = [];
643
574
  // Extract properties
644
575
  configObj.getProperties().forEach((prop) => {
645
576
  if (Node.isPropertyAssignment(prop)) {
646
577
  const name = prop.getName();
647
- const value = prop.getInitializer()?.getText();
648
- if (name === 'description' && value) {
649
- description = value.replace(/['"]/g, '');
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, '');
650
587
  }
651
588
  else if (name === 'context' && value) {
652
- context = value.replace(/['"]/g, '');
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);
653
600
  }
654
601
  }
655
602
  });
603
+ if (skillName) {
604
+ skills.push({
605
+ name: skillName,
606
+ version: skillVersion || '1.0.0',
607
+ description,
608
+ context,
609
+ constructorTools
610
+ });
611
+ }
656
612
  }
657
613
  }
658
614
  }
659
615
  });
660
- return { skillId, description, context };
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;
676
+ }
677
+ else {
678
+ // Update existing skill with new skillId
679
+ existingSkill.skillId = newSkillId;
680
+ yamlUpdated = true;
681
+ }
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
+ }
687
+ }
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
691
+ }
692
+ }
693
+ }
694
+ // Update YAML file if needed
695
+ if (yamlUpdated) {
696
+ await updateYamlWithSkills(existingSkills, config);
697
+ }
698
+ return updatedSkillsArray;
699
+ }
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
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');
661
725
  }