@arcteninc/core 0.0.48 → 0.0.50

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.50",
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,306 @@ 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
+ // Handle interfaces and type aliases (for recursive types)
752
+ if (ts.isInterfaceDeclaration(firstDecl) || ts.isTypeAliasDeclaration(firstDecl)) {
753
+ const typeName = symbol.getName();
754
+ if (typeName && defs) {
755
+ const defsKey = typeName; // Use the type name as key
756
+
757
+ // Check if we've visited this type before (circular/recursive reference)
758
+ if (visited.has(typeId)) {
759
+ // Create a reference to avoid infinite recursion
760
+ if (!defs[defsKey]) {
761
+ // Create placeholder - will be filled in below
762
+ defs[defsKey] = { type: 'object', properties: {} };
763
+ }
764
+ return { isOptional: false, schema: { $ref: `#/$defs/${defsKey}` } };
765
+ }
766
+
767
+ // Check if already defined (from a previous call)
768
+ if (defs[defsKey]) {
769
+ return { isOptional: false, schema: { $ref: `#/$defs/${defsKey}` } };
770
+ }
771
+
772
+ // Mark as visited and build the definition
773
+ visited.add(typeId);
774
+
775
+ // Build the schema for this type
776
+ const props = checker.getPropertiesOfType(type);
777
+ if (props.length > 0) {
778
+ const properties: Record<string, JsonSchemaProperty> = {};
779
+ const required: string[] = [];
780
+
781
+ // Use the same visited set for properties to detect recursion
782
+ for (const prop of props) {
783
+ const propName = prop.getName();
784
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0] || checker.getDeclaredTypeOfSymbol(prop).symbol?.valueDeclaration || null as any);
785
+ const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
786
+
787
+ // Recursively serialize the property type (will detect recursion via visited set)
788
+ const propResult = serializeTypeCustom(propType, checker, defs, visited, depth + 1);
789
+ properties[propName] = propResult.schema;
790
+
791
+ if (!isOptional && !propResult.isOptional) {
792
+ required.push(propName);
793
+ }
794
+ }
795
+
796
+ const schema: JsonSchemaProperty = {
797
+ type: 'object',
798
+ properties,
799
+ ...(required.length > 0 && { required })
800
+ };
801
+
802
+ // Store in defs
803
+ defs[defsKey] = schema;
804
+
805
+ // Return reference
806
+ return { isOptional: false, schema: { $ref: `#/$defs/${defsKey}` } };
807
+ }
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ // Handle object types with properties (anonymous types)
814
+ if (type.flags & ts.TypeFlags.Object) {
815
+ const props = checker.getPropertiesOfType(type);
816
+ if (props.length > 0) {
817
+ const properties: Record<string, JsonSchemaProperty> = {};
818
+ const required: string[] = [];
819
+
820
+ for (const prop of props) {
821
+ const propName = prop.getName();
822
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0] || checker.getDeclaredTypeOfSymbol(prop).symbol?.valueDeclaration || null as any);
823
+ const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
824
+
825
+ // Recursively serialize the property type
826
+ const propResult = serializeTypeCustom(propType, checker, defs, new Set(visited), depth + 1);
827
+ properties[propName] = propResult.schema;
828
+
829
+ if (!isOptional && !propResult.isOptional) {
830
+ required.push(propName);
831
+ }
832
+ }
833
+
834
+ return {
835
+ isOptional: false,
836
+ schema: {
837
+ type: 'object',
838
+ properties,
839
+ ...(required.length > 0 && { required })
840
+ }
841
+ };
842
+ }
843
+ }
844
+
845
+ // Fallback to empty schema
846
+ return { isOptional: false, schema: {} };
847
+ }
848
+
752
849
  /**
753
850
  * 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
851
+ * Handles primitives, arrays, and named types (interfaces, type aliases, classes, enums)
756
852
  */
757
853
  async function serializeType(
758
854
  type: ts.Type,
759
855
  checker: ts.TypeChecker,
760
856
  program: ts.Program | undefined,
761
857
  configPath: string | undefined,
762
- defs?: Record<string, JsonSchemaProperty>,
763
- sourceFileForLookup?: ts.SourceFile
858
+ defs?: Record<string, JsonSchemaProperty>
764
859
  ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
765
- if (!program || !configPath) {
766
- return { isOptional: false, schema: {} }; // Empty schema = any value
860
+ // Handle primitives first (these don't need the generator)
861
+ if (type.flags & ts.TypeFlags.String) {
862
+ return { isOptional: false, schema: { type: 'string' } };
863
+ }
864
+ if (type.flags & ts.TypeFlags.Number) {
865
+ return { isOptional: false, schema: { type: 'number' } };
866
+ }
867
+ if (type.flags & ts.TypeFlags.Boolean) {
868
+ return { isOptional: false, schema: { type: 'boolean' } };
869
+ }
870
+ if (type.flags & ts.TypeFlags.Null) {
871
+ return { isOptional: false, schema: { type: 'null' } };
872
+ }
873
+ if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
874
+ return { isOptional: true, schema: { type: 'null' } };
767
875
  }
768
876
 
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];
877
+ // Handle string/number literals (enum values are often represented as literals)
878
+ if (type.flags & ts.TypeFlags.StringLiteral) {
879
+ const literalType = type as ts.StringLiteralType;
880
+ return { isOptional: false, schema: { type: 'string', const: literalType.value } };
881
+ }
882
+ if (type.flags & ts.TypeFlags.NumberLiteral) {
883
+ const literalType = type as ts.NumberLiteralType;
884
+ return { isOptional: false, schema: { type: 'number', const: literalType.value } };
885
+ }
886
+
887
+ // Handle arrays
888
+ if (checker.isArrayType(type)) {
889
+ const typeArgs = (type as ts.TypeReference).typeArguments;
890
+ if (typeArgs && typeArgs.length > 0) {
891
+ const itemResult = await serializeType(typeArgs[0], checker, program, configPath, defs);
892
+ return {
893
+ isOptional: false,
894
+ schema: { type: 'array', items: itemResult.schema }
895
+ };
778
896
  }
897
+ return { isOptional: false, schema: { type: 'array' } };
779
898
  }
780
-
781
- const typeInfo = getTypeInfoForGenerator(type, checker, lookupFile);
899
+
900
+ // Handle union types (like string | null)
901
+ if (type.isUnion()) {
902
+ const types = type.types;
903
+ const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
904
+ const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
905
+ const nonNullUndefinedTypes = types.filter(
906
+ t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
907
+ );
908
+
909
+ // If it's just T | undefined, make it optional
910
+ if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
911
+ const result = await serializeType(nonNullUndefinedTypes[0], checker, program, configPath, defs);
912
+ return { isOptional: true, schema: result.schema };
913
+ }
914
+
915
+ // Check if union is all string/number literals (enum-like union)
916
+ // This handles cases where TypeScript represents enums as unions of literals
917
+ const allStringLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.StringLiteral);
918
+ const allNumberLiterals = nonNullUndefinedTypes.every(t => t.flags & ts.TypeFlags.NumberLiteral);
919
+
920
+ if (allStringLiterals && nonNullUndefinedTypes.length > 0) {
921
+ const enumValues = nonNullUndefinedTypes.map(t => {
922
+ const literalType = t as ts.StringLiteralType;
923
+ return literalType.value;
924
+ });
925
+ const schema: JsonSchemaProperty = { enum: enumValues };
926
+ return { isOptional: hasNull || hasUndefined, schema };
927
+ }
928
+
929
+ if (allNumberLiterals && nonNullUndefinedTypes.length > 0) {
930
+ const enumValues = nonNullUndefinedTypes.map(t => {
931
+ const literalType = t as ts.NumberLiteralType;
932
+ return literalType.value;
933
+ });
934
+ const schema: JsonSchemaProperty = { enum: enumValues };
935
+ return { isOptional: hasNull || hasUndefined, schema };
936
+ }
937
+
938
+ // Build anyOf for unions
939
+ if (nonNullUndefinedTypes.length > 0 || hasNull) {
940
+ const anyOf: JsonSchemaProperty[] = [];
941
+ if (hasNull || hasUndefined) {
942
+ anyOf.push({ type: 'null' });
943
+ }
944
+ for (const unionType of nonNullUndefinedTypes) {
945
+ const result = await serializeType(unionType, checker, program, configPath, defs);
946
+ anyOf.push(result.schema);
947
+ }
948
+ if (anyOf.length === 1) {
949
+ return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
950
+ }
951
+ return { isOptional: false, schema: { anyOf } };
952
+ }
953
+ }
954
+
955
+ // Check if it's an enum - handle with custom serializer first
956
+ const symbol = type.getSymbol();
957
+ if (symbol) {
958
+ const declarations = symbol.getDeclarations();
959
+ if (declarations && declarations.length > 0) {
960
+ const firstDecl = declarations[0];
961
+ if (ts.isEnumDeclaration(firstDecl)) {
962
+ // Use custom serializer for enums (more reliable)
963
+ return serializeTypeCustom(type, checker, defs);
964
+ }
965
+ }
966
+ }
967
+
968
+ // For named types (interfaces, type aliases, classes), try generator first
969
+ if (!program || !configPath) {
970
+ // No program/config - use custom serializer
971
+ return serializeTypeCustom(type, checker, defs);
972
+ }
973
+
974
+ const typeInfo = getTypeInfoForGenerator(type, checker);
782
975
  if (!typeInfo) {
783
- // Anonymous/inline type - can't serialize with generator
784
- return { isOptional: false, schema: {} }; // Empty schema = any value
976
+ // Anonymous/inline type - use custom serializer
977
+ return serializeTypeCustom(type, checker, defs);
785
978
  }
786
979
 
787
980
  // Check cache first
@@ -790,15 +983,16 @@ async function serializeType(
790
983
  return generatorCache.get(cacheKey)!;
791
984
  }
792
985
 
793
- // Use generator
986
+ // Use generator for named types
794
987
  const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
795
988
  if (result) {
796
989
  generatorCache.set(cacheKey, result);
797
990
  return result;
798
991
  }
799
992
 
800
- // Generator failed - return empty schema
801
- return { isOptional: false, schema: {} }; // Empty schema = any value
993
+ // Generator failed - fall back to custom serializer
994
+ console.warn(`⚠️ Failed to generate schema for type "${typeInfo.typeName}" from ${typeInfo.sourceFile}. Falling back to custom serializer.`);
995
+ return serializeTypeCustom(type, checker, defs);
802
996
  }
803
997
 
804
998
  /**
@@ -999,8 +1193,7 @@ async function extractFunctionMetadata(
999
1193
 
1000
1194
  // Use ts-json-schema-generator for named types only
1001
1195
  // 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);
1196
+ const result = await serializeType(type, checker, program, configPath, defs);
1004
1197
  propSchema = result.schema;
1005
1198
  isOptional = result.isOptional;
1006
1199
  } else {