@arcteninc/core 0.0.48 → 0.0.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcteninc/core",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -21,9 +21,11 @@ async function ensureGeneratorLoaded(): Promise<boolean> {
21
21
  // @ts-ignore - ts-json-schema-generator is optional
22
22
  tsJsonSchemaGeneratorModule = await import('ts-json-schema-generator');
23
23
  generatorAvailable = true;
24
+ console.log('✓ ts-json-schema-generator loaded successfully');
24
25
  return true;
25
26
  } catch (error) {
26
27
  tsJsonSchemaGeneratorModule = false; // Mark as failed
28
+ console.warn('⚠️ ts-json-schema-generator not available:', error);
27
29
  return false;
28
30
  }
29
31
  }
@@ -572,108 +574,41 @@ function resolveImportPath(
572
574
  return null;
573
575
  }
574
576
 
575
- /**
576
- * Try to resolve an anonymous type to a named type by checking structural compatibility
577
- * This helps when function parameters use anonymous types that match existing interfaces
578
- */
579
- function tryResolveToNamedType(
580
- type: ts.Type,
581
- checker: ts.TypeChecker,
582
- sourceFile: ts.SourceFile
583
- ): { typeName: string; sourceFile: string } | null {
584
- // Only try for object types
585
- if (!(type.flags & ts.TypeFlags.Object)) return null;
586
-
587
- const props = checker.getPropertiesOfType(type);
588
- if (props.length === 0) return null;
589
-
590
- // Get all exported interfaces/types from the same file
591
- const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
592
- if (!moduleSymbol) return null;
593
-
594
- const fileSymbols = checker.getExportsOfModule(moduleSymbol);
595
- if (!fileSymbols || fileSymbols.length === 0) return null;
596
-
597
- // Check each exported type/interface to see if it matches structurally
598
- for (const symbol of fileSymbols) {
599
- if (!symbol) continue;
600
-
601
- const name = symbol.getName();
602
- if (!name || name.length === 0) continue;
603
-
604
- const declarations = symbol.getDeclarations();
605
- if (!declarations || declarations.length === 0) continue;
606
-
607
- for (const decl of declarations) {
608
- if (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) {
609
- // Get the type from the declaration
610
- const declaredType = checker.getTypeAtLocation(decl);
611
-
612
- // Check if properties match (simple structural check)
613
- const declaredProps = checker.getPropertiesOfType(declaredType);
614
- if (declaredProps.length === props.length) {
615
- // Check if all property names match
616
- const propNames = new Set(props.map(p => p.getName()));
617
- const declaredPropNames = new Set(declaredProps.map(p => p.getName()));
618
-
619
- if (propNames.size === declaredPropNames.size &&
620
- [...propNames].every(n => declaredPropNames.has(n))) {
621
- // Properties match - this might be the same type
622
- return {
623
- typeName: name,
624
- sourceFile: sourceFile.fileName,
625
- };
626
- }
627
- }
628
- }
629
- }
630
- }
631
-
632
- return null;
633
- }
634
-
635
577
  /**
636
578
  * Try to get type name and source file for use with ts-json-schema-generator
579
+ * Handles interfaces, type aliases, classes, and enums
637
580
  */
638
581
  function getTypeInfoForGenerator(
639
582
  type: ts.Type,
640
- checker: ts.TypeChecker,
641
- paramSourceFile?: ts.SourceFile
583
+ checker: ts.TypeChecker
642
584
  ): { typeName: string; sourceFile: string } | null {
643
585
  const symbol = type.getSymbol();
644
-
645
- // If it's a named type, use it directly
646
- if (symbol) {
647
- const name = symbol.getName();
648
- if (name && name.length > 0) {
649
- const declarations = symbol.getDeclarations();
650
- if (declarations && declarations.length > 0) {
651
- // Find the first declaration that's a type alias, interface, or class
652
- for (const decl of declarations) {
653
- if (
654
- ts.isTypeAliasDeclaration(decl) ||
655
- ts.isInterfaceDeclaration(decl) ||
656
- ts.isClassDeclaration(decl)
657
- ) {
658
- const sourceFile = decl.getSourceFile();
659
- if (sourceFile) {
660
- return {
661
- typeName: name,
662
- sourceFile: sourceFile.fileName,
663
- };
664
- }
665
- }
666
- }
586
+ if (!symbol) return null;
587
+
588
+ const name = symbol.getName();
589
+ if (!name || name.length === 0) return null;
590
+
591
+ const declarations = symbol.getDeclarations();
592
+ if (!declarations || declarations.length === 0) return null;
593
+
594
+ // Find the first declaration that's a type alias, interface, class, or enum
595
+ for (const decl of declarations) {
596
+ if (
597
+ ts.isTypeAliasDeclaration(decl) ||
598
+ ts.isInterfaceDeclaration(decl) ||
599
+ ts.isClassDeclaration(decl) ||
600
+ ts.isEnumDeclaration(decl)
601
+ ) {
602
+ const sourceFile = decl.getSourceFile();
603
+ if (sourceFile) {
604
+ return {
605
+ typeName: name,
606
+ sourceFile: sourceFile.fileName,
607
+ };
667
608
  }
668
609
  }
669
610
  }
670
-
671
- // If it's an anonymous type, try to resolve it to a named type
672
- if (paramSourceFile) {
673
- const resolved = tryResolveToNamedType(type, checker, paramSourceFile);
674
- if (resolved) return resolved;
675
- }
676
-
611
+
677
612
  return null;
678
613
  }
679
614
 
@@ -695,17 +630,7 @@ async function serializeTypeWithGenerator(
695
630
  return null;
696
631
  }
697
632
 
698
- // Try to get source file from the type's declarations or use a fallback
699
- let sourceFileForLookup: ts.SourceFile | undefined;
700
- const symbol = type.getSymbol();
701
- if (symbol) {
702
- const declarations = symbol.getDeclarations();
703
- if (declarations && declarations.length > 0) {
704
- sourceFileForLookup = declarations[0].getSourceFile();
705
- }
706
- }
707
-
708
- const typeInfo = getTypeInfoForGenerator(type, checker, sourceFileForLookup);
633
+ const typeInfo = getTypeInfoForGenerator(type, checker);
709
634
  if (!typeInfo) return null;
710
635
 
711
636
  try {
@@ -741,7 +666,8 @@ async function serializeTypeWithGenerator(
741
666
  schema: schemaWithoutDefs as JsonSchemaProperty,
742
667
  };
743
668
  } catch (error) {
744
- // If generator fails, return null (will result in empty schema)
669
+ // If generator fails, log the error for debugging
670
+ console.warn(`⚠️ ts-json-schema-generator failed for type "${typeInfo.typeName}" from ${typeInfo.sourceFile}:`, error);
745
671
  return null;
746
672
  }
747
673
  }
@@ -749,39 +675,246 @@ async function serializeTypeWithGenerator(
749
675
  // Cache for generator results to avoid repeated calls
750
676
  const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
751
677
 
678
+ /**
679
+ * Custom serializer for anonymous types, enums, and object types
680
+ * This is a fallback when ts-json-schema-generator can't handle the type
681
+ */
682
+ function serializeTypeCustom(
683
+ type: ts.Type,
684
+ checker: ts.TypeChecker,
685
+ defs?: Record<string, JsonSchemaProperty>,
686
+ visited = new Set<number>(),
687
+ depth = 0
688
+ ): { isOptional: boolean; schema: JsonSchemaProperty } {
689
+ // Prevent infinite recursion
690
+ if (depth > 10) {
691
+ return { isOptional: false, schema: {} };
692
+ }
693
+
694
+ const typeId = (type as any).id;
695
+ if (typeId !== undefined && visited.has(typeId)) {
696
+ return { isOptional: false, schema: {} };
697
+ }
698
+
699
+ if (typeId !== undefined) {
700
+ visited.add(typeId);
701
+ }
702
+
703
+ // Handle primitives first (safety net in case serializeType didn't catch them)
704
+ if (type.flags & ts.TypeFlags.String) {
705
+ return { isOptional: false, schema: { type: 'string' } };
706
+ }
707
+ if (type.flags & ts.TypeFlags.Number) {
708
+ return { isOptional: false, schema: { type: 'number' } };
709
+ }
710
+ if (type.flags & ts.TypeFlags.Boolean) {
711
+ return { isOptional: false, schema: { type: 'boolean' } };
712
+ }
713
+ if (type.flags & ts.TypeFlags.Null) {
714
+ return { isOptional: false, schema: { type: 'null' } };
715
+ }
716
+
717
+ // Handle string/number literals (enum values are often represented as literals)
718
+ if (type.flags & ts.TypeFlags.StringLiteral) {
719
+ const literalType = type as ts.StringLiteralType;
720
+ return { isOptional: false, schema: { type: 'string', const: literalType.value } };
721
+ }
722
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
723
+ const literalType = type as ts.NumberLiteralType;
724
+ return { isOptional: false, schema: { type: 'number', const: literalType.value } };
725
+ }
726
+
727
+ // Handle enums (TypeScript enum types)
728
+ const symbol = type.getSymbol();
729
+ if (symbol) {
730
+ const declarations = symbol.getDeclarations();
731
+ if (declarations && declarations.length > 0) {
732
+ const firstDecl = declarations[0];
733
+ if (ts.isEnumDeclaration(firstDecl)) {
734
+ const enumMembers = firstDecl.members.map(m => {
735
+ if (m.initializer && ts.isStringLiteral(m.initializer)) {
736
+ return m.initializer.text;
737
+ } else if (m.initializer && ts.isNumericLiteral(m.initializer)) {
738
+ return Number(m.initializer.text);
739
+ } else if (m.name && ts.isIdentifier(m.name)) {
740
+ // For enum members without initializers, use the name
741
+ return m.name.text;
742
+ }
743
+ return null;
744
+ }).filter((v): v is string | number => v !== null);
745
+
746
+ if (enumMembers.length > 0) {
747
+ return { isOptional: false, schema: { enum: enumMembers } };
748
+ }
749
+ }
750
+ }
751
+ }
752
+
753
+ // Handle object types with properties
754
+ if (type.flags & ts.TypeFlags.Object) {
755
+ const props = checker.getPropertiesOfType(type);
756
+ if (props.length > 0) {
757
+ const properties: Record<string, JsonSchemaProperty> = {};
758
+ const required: string[] = [];
759
+
760
+ for (const prop of props) {
761
+ const propName = prop.getName();
762
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0] || checker.getDeclaredTypeOfSymbol(prop).symbol?.valueDeclaration || null as any);
763
+ const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
764
+
765
+ // Recursively serialize the property type
766
+ const propResult = serializeTypeCustom(propType, checker, defs, new Set(visited), depth + 1);
767
+ properties[propName] = propResult.schema;
768
+
769
+ if (!isOptional && !propResult.isOptional) {
770
+ required.push(propName);
771
+ }
772
+ }
773
+
774
+ return {
775
+ isOptional: false,
776
+ schema: {
777
+ type: 'object',
778
+ properties,
779
+ ...(required.length > 0 && { required })
780
+ }
781
+ };
782
+ }
783
+ }
784
+
785
+ // Fallback to empty schema
786
+ return { isOptional: false, schema: {} };
787
+ }
788
+
752
789
  /**
753
790
  * Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
754
- * Only works for named types (interfaces, type aliases, classes)
755
- * Returns empty schema for anonymous types
791
+ * Handles primitives, arrays, and named types (interfaces, type aliases, classes, enums)
756
792
  */
757
793
  async function serializeType(
758
794
  type: ts.Type,
759
795
  checker: ts.TypeChecker,
760
796
  program: ts.Program | undefined,
761
797
  configPath: string | undefined,
762
- defs?: Record<string, JsonSchemaProperty>,
763
- sourceFileForLookup?: ts.SourceFile
798
+ defs?: Record<string, JsonSchemaProperty>
764
799
  ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
765
- if (!program || !configPath) {
766
- return { isOptional: false, schema: {} }; // Empty schema = any value
800
+ // Handle primitives first (these don't need the generator)
801
+ if (type.flags & ts.TypeFlags.String) {
802
+ return { isOptional: false, schema: { type: 'string' } };
803
+ }
804
+ if (type.flags & ts.TypeFlags.Number) {
805
+ return { isOptional: false, schema: { type: 'number' } };
806
+ }
807
+ if (type.flags & ts.TypeFlags.Boolean) {
808
+ return { isOptional: false, schema: { type: 'boolean' } };
809
+ }
810
+ if (type.flags & ts.TypeFlags.Null) {
811
+ return { isOptional: false, schema: { type: 'null' } };
812
+ }
813
+ if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
814
+ return { isOptional: true, schema: { type: 'null' } };
815
+ }
816
+
817
+ // Handle string/number literals (enum values are often represented as literals)
818
+ if (type.flags & ts.TypeFlags.StringLiteral) {
819
+ const literalType = type as ts.StringLiteralType;
820
+ return { isOptional: false, schema: { type: 'string', const: literalType.value } };
821
+ }
822
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
823
+ const literalType = type as ts.NumberLiteralType;
824
+ return { isOptional: false, schema: { type: 'number', const: literalType.value } };
767
825
  }
768
826
 
769
- // Use provided sourceFileForLookup or try to find one from program
770
- let lookupFile = sourceFileForLookup;
771
- if (!lookupFile && program) {
772
- // Try to find a source file that might contain the type
773
- // This is a best-effort approach for anonymous types
774
- const sourceFiles = program.getSourceFiles();
775
- if (sourceFiles.length > 0) {
776
- // Use the first source file as a fallback (could be improved)
777
- lookupFile = sourceFiles[0];
827
+ // Handle arrays
828
+ if (checker.isArrayType(type)) {
829
+ const typeArgs = (type as ts.TypeReference).typeArguments;
830
+ if (typeArgs && typeArgs.length > 0) {
831
+ const itemResult = await serializeType(typeArgs[0], checker, program, configPath, defs);
832
+ return {
833
+ isOptional: false,
834
+ schema: { type: 'array', items: itemResult.schema }
835
+ };
778
836
  }
837
+ return { isOptional: false, schema: { type: 'array' } };
779
838
  }
780
-
781
- const typeInfo = getTypeInfoForGenerator(type, checker, lookupFile);
839
+
840
+ // Handle union types (like string | null)
841
+ if (type.isUnion()) {
842
+ const types = type.types;
843
+ const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
844
+ const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
845
+ const nonNullUndefinedTypes = types.filter(
846
+ t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
847
+ );
848
+
849
+ // If it's just T | undefined, make it optional
850
+ if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
851
+ const result = await serializeType(nonNullUndefinedTypes[0], checker, program, configPath, defs);
852
+ return { isOptional: true, schema: result.schema };
853
+ }
854
+
855
+ // Check if union is all string/number literals (enum-like union)
856
+ // This handles cases where TypeScript represents enums as unions of literals
857
+ const allStringLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.StringLiteral);
858
+ const allNumberLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.NumberLiteral);
859
+
860
+ if (allStringLiterals && nonNullUndefinedTypes.length > 0) {
861
+ const enumValues = nonNullUndefinedTypes.map(t => {
862
+ const literalType = t as ts.StringLiteralType;
863
+ return literalType.value;
864
+ });
865
+ const schema: JsonSchemaProperty = { enum: enumValues };
866
+ return { isOptional: hasNull || hasUndefined, schema };
867
+ }
868
+
869
+ if (allNumberLiterals && nonNullUndefinedTypes.length > 0) {
870
+ const enumValues = nonNullUndefinedTypes.map(t => {
871
+ const literalType = t as ts.NumberLiteralType;
872
+ return literalType.value;
873
+ });
874
+ const schema: JsonSchemaProperty = { enum: enumValues };
875
+ return { isOptional: hasNull || hasUndefined, schema };
876
+ }
877
+
878
+ // Build anyOf for unions
879
+ if (nonNullUndefinedTypes.length > 0 || hasNull) {
880
+ const anyOf: JsonSchemaProperty[] = [];
881
+ if (hasNull || hasUndefined) {
882
+ anyOf.push({ type: 'null' });
883
+ }
884
+ for (const unionType of nonNullUndefinedTypes) {
885
+ const result = await serializeType(unionType, checker, program, configPath, defs);
886
+ anyOf.push(result.schema);
887
+ }
888
+ if (anyOf.length === 1) {
889
+ return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
890
+ }
891
+ return { isOptional: false, schema: { anyOf } };
892
+ }
893
+ }
894
+
895
+ // Check if it's an enum - handle with custom serializer first
896
+ const symbol = type.getSymbol();
897
+ if (symbol) {
898
+ const declarations = symbol.getDeclarations();
899
+ if (declarations && declarations.length > 0) {
900
+ const firstDecl = declarations[0];
901
+ if (ts.isEnumDeclaration(firstDecl)) {
902
+ // Use custom serializer for enums (more reliable)
903
+ return serializeTypeCustom(type, checker, defs);
904
+ }
905
+ }
906
+ }
907
+
908
+ // For named types (interfaces, type aliases, classes), try generator first
909
+ if (!program || !configPath) {
910
+ // No program/config - use custom serializer
911
+ return serializeTypeCustom(type, checker, defs);
912
+ }
913
+
914
+ const typeInfo = getTypeInfoForGenerator(type, checker);
782
915
  if (!typeInfo) {
783
- // Anonymous/inline type - can't serialize with generator
784
- return { isOptional: false, schema: {} }; // Empty schema = any value
916
+ // Anonymous/inline type - use custom serializer
917
+ return serializeTypeCustom(type, checker, defs);
785
918
  }
786
919
 
787
920
  // Check cache first
@@ -790,14 +923,15 @@ async function serializeType(
790
923
  return generatorCache.get(cacheKey)!;
791
924
  }
792
925
 
793
- // Use generator
926
+ // Use generator for named types
794
927
  const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
795
928
  if (result) {
796
929
  generatorCache.set(cacheKey, result);
797
930
  return result;
798
931
  }
799
932
 
800
- // Generator failed - return empty schema
933
+ // Generator failed - log and return empty schema
934
+ console.warn(`⚠️ Failed to generate schema for type "${typeInfo.typeName}" from ${typeInfo.sourceFile}. Using empty schema.`);
801
935
  return { isOptional: false, schema: {} }; // Empty schema = any value
802
936
  }
803
937
 
@@ -999,8 +1133,7 @@ async function extractFunctionMetadata(
999
1133
 
1000
1134
  // Use ts-json-schema-generator for named types only
1001
1135
  // Anonymous types are not supported - agents can't use them anyway
1002
- // Pass sourceFile to help resolve anonymous types to named types
1003
- const result = await serializeType(type, checker, program, configPath, defs, sourceFile);
1136
+ const result = await serializeType(type, checker, program, configPath, defs);
1004
1137
  propSchema = result.schema;
1005
1138
  isOptional = result.isOptional;
1006
1139
  } else {