@arcteninc/core 0.0.46 → 0.0.48

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.46",
3
+ "version": "0.0.48",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -46,6 +46,7 @@
46
46
  "ajv": "^8.17.1",
47
47
  "react": "^19.2.0",
48
48
  "react-dom": "^19.2.0",
49
+ "ts-json-schema-generator": "^2.4.0",
49
50
  "typescript": "^5.9.3",
50
51
  "vite": "^7.1.12",
51
52
  "vite-plugin-dts": "^4.5.4"
@@ -9,6 +9,25 @@ import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { glob } from 'glob';
11
11
 
12
+ // Lazy import for ts-json-schema-generator (optional - falls back to custom serializer if not available)
13
+ let tsJsonSchemaGeneratorModule: any = null;
14
+ let generatorAvailable = false;
15
+
16
+ async function ensureGeneratorLoaded(): Promise<boolean> {
17
+ if (generatorAvailable) return true;
18
+ if (tsJsonSchemaGeneratorModule !== null) return false; // Already tried and failed
19
+
20
+ try {
21
+ // @ts-ignore - ts-json-schema-generator is optional
22
+ tsJsonSchemaGeneratorModule = await import('ts-json-schema-generator');
23
+ generatorAvailable = true;
24
+ return true;
25
+ } catch (error) {
26
+ tsJsonSchemaGeneratorModule = false; // Mark as failed
27
+ return false;
28
+ }
29
+ }
30
+
12
31
  // JSON Schema compatible format
13
32
  interface JsonSchemaProperty {
14
33
  type?: string | string[]; // Can be string, array of strings, or omitted for anyOf/oneOf/$ref
@@ -553,692 +572,233 @@ function resolveImportPath(
553
572
  return null;
554
573
  }
555
574
 
556
- // Constants for JSON Schema validation
557
- // These are actual constants from the JSON Schema specification
558
- const VALID_JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
559
-
560
- // Safety limit for serialization depth (only used as a last resort)
561
- // Note: Cycle detection via 'visited' Set is the primary protection against infinite recursion
562
- // This is only a fallback safety net for edge cases
563
- const MAX_SERIALIZATION_DEPTH = 50; // Increased from 10 - legitimate types can be deeply nested
564
-
565
- // Note: MAX_TOOL_NAME_LENGTH was removed - we now use proper AST node type checks
566
- // instead of string length heuristics for better accuracy
567
-
568
- /**
569
- * Type-safe accessors for TypeScript internal APIs
570
- * These use type guards to safely access properties that aren't in the public API
571
- */
572
-
573
- interface BooleanLiteralType extends ts.Type {
574
- intrinsicName: 'true' | 'false';
575
- }
576
-
577
- interface NumberLiteralType extends ts.Type {
578
- value: number;
579
- }
580
-
581
- interface StringLiteralType extends ts.Type {
582
- value: string;
583
- }
584
-
585
- /**
586
- * Get boolean literal value from a TypeScript type
587
- */
588
- function getBooleanLiteralValue(type: ts.Type): boolean | null {
589
- if (type.flags & ts.TypeFlags.BooleanLiteral) {
590
- const boolType = type as BooleanLiteralType;
591
- return boolType.intrinsicName === 'true';
592
- }
593
- return null;
594
- }
595
-
596
- /**
597
- * Get number literal value from a TypeScript type
598
- */
599
- function getNumberLiteralValue(type: ts.Type): number | null {
600
- if (type.flags & ts.TypeFlags.NumberLiteral) {
601
- const numType = type as NumberLiteralType;
602
- return numType.value;
603
- }
604
- return null;
605
- }
606
-
607
575
  /**
608
- * Get string literal value from a TypeScript type
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
609
578
  */
610
- function getStringLiteralValue(type: ts.Type): string | null {
611
- if (type.flags & ts.TypeFlags.StringLiteral) {
612
- const strType = type as StringLiteralType;
613
- return strType.value;
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
+ }
614
630
  }
631
+
615
632
  return null;
616
633
  }
617
634
 
618
635
  /**
619
- * Get type ID safely (for cycle detection)
620
- * Uses TypeScript's internal API - this is necessary for cycle detection
621
- * but we wrap it in a function to centralize the unsafe access
622
- */
623
- function getTypeId(type: ts.Type): number | undefined {
624
- // TypeScript's Type interface doesn't expose 'id' in public API,
625
- // but it's available internally and needed for cycle detection.
626
- // This is a known limitation when working with TypeScript's compiler API.
627
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
628
- return (type as any).id;
629
- }
630
-
631
- /**
632
- * Get a stable name for a type to use in $defs
633
- * Returns the type name if it's a named type (interface, type alias, class)
634
- * Otherwise returns a generated name based on type ID
636
+ * Try to get type name and source file for use with ts-json-schema-generator
635
637
  */
636
- function getTypeNameForDefs(
638
+ function getTypeInfoForGenerator(
637
639
  type: ts.Type,
638
- checker: ts.TypeChecker
639
- ): string | null {
640
+ checker: ts.TypeChecker,
641
+ paramSourceFile?: ts.SourceFile
642
+ ): { typeName: string; sourceFile: string } | null {
640
643
  const symbol = type.getSymbol();
644
+
645
+ // If it's a named type, use it directly
641
646
  if (symbol) {
642
647
  const name = symbol.getName();
643
648
  if (name && name.length > 0) {
644
- // Check if it's a named type (not a primitive)
645
649
  const declarations = symbol.getDeclarations();
646
650
  if (declarations && declarations.length > 0) {
647
- // Check if it's a type alias, interface, or class
651
+ // Find the first declaration that's a type alias, interface, or class
648
652
  for (const decl of declarations) {
649
653
  if (
650
654
  ts.isTypeAliasDeclaration(decl) ||
651
655
  ts.isInterfaceDeclaration(decl) ||
652
656
  ts.isClassDeclaration(decl)
653
657
  ) {
654
- return name;
658
+ const sourceFile = decl.getSourceFile();
659
+ if (sourceFile) {
660
+ return {
661
+ typeName: name,
662
+ sourceFile: sourceFile.fileName,
663
+ };
664
+ }
655
665
  }
656
666
  }
657
667
  }
658
668
  }
659
669
  }
660
670
 
661
- // Fallback: try to get a name from typeToString
662
- const typeString = checker.typeToString(type);
663
- // If it's a simple type name (not a complex expression), use it
664
- if (typeString && !typeString.includes('|') && !typeString.includes('&') &&
665
- !typeString.includes('<') && !typeString.includes('(') &&
666
- typeString.length < 50 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(typeString)) {
667
- return typeString;
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;
668
675
  }
669
676
 
670
677
  return null;
671
678
  }
672
679
 
673
680
  /**
674
- * Serialize a literal type (boolean, number, or string literal) to JSON Schema
675
- * Returns null if the type is not a literal
681
+ * Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
682
+ * Returns: { isOptional, schema } or null if generator not available
683
+ * Note: This is async and should only be called at top level
676
684
  */
677
- function serializeLiteralType(type: ts.Type): JsonSchemaProperty | null {
678
- const boolValue = getBooleanLiteralValue(type);
679
- if (boolValue !== null) {
680
- return { type: 'boolean', const: boolValue };
681
- }
682
-
683
- const numValue = getNumberLiteralValue(type);
684
- if (numValue !== null) {
685
- return { type: 'number', const: numValue };
686
- }
687
-
688
- const strValue = getStringLiteralValue(type);
689
- if (strValue !== null) {
690
- return { type: 'string', const: strValue };
685
+ async function serializeTypeWithGenerator(
686
+ type: ts.Type,
687
+ checker: ts.TypeChecker,
688
+ program: ts.Program,
689
+ configPath: string | undefined,
690
+ defs?: Record<string, JsonSchemaProperty>
691
+ ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty } | null> {
692
+ // Try to load generator if not already loaded
693
+ const generatorLoaded = await ensureGeneratorLoaded();
694
+ if (!generatorLoaded || !tsJsonSchemaGeneratorModule) {
695
+ return null;
691
696
  }
692
-
693
- return null;
694
- }
695
697
 
696
- /**
697
- * Check if a type is a type reference (named type like FilterTreeNode)
698
- * Uses TypeScript API to determine if it's a named type reference
699
- */
700
- function isTypeReference(type: ts.Type, checker: ts.TypeChecker): boolean {
698
+ // Try to get source file from the type's declarations or use a fallback
699
+ let sourceFileForLookup: ts.SourceFile | undefined;
701
700
  const symbol = type.getSymbol();
702
- if (!symbol) return false;
703
-
704
- const name = symbol.getName();
705
- if (name.length === 0) return false;
706
-
707
- // Check if it's a primitive JSON Schema type (should not be treated as type reference)
708
- if (VALID_JSON_SCHEMA_TYPES.has(name.toLowerCase())) {
709
- return false;
710
- }
711
-
712
- // Check if it's a named type (has declarations and is not a primitive)
713
- const declarations = symbol.getDeclarations();
714
- if (!declarations || declarations.length === 0) return false;
715
-
716
- // Check if any declaration is a type alias, interface, or class
717
- // This is more reliable than regex pattern matching
718
- for (const decl of declarations) {
719
- if (
720
- ts.isTypeAliasDeclaration(decl) ||
721
- ts.isInterfaceDeclaration(decl) ||
722
- ts.isClassDeclaration(decl) ||
723
- ts.isEnumDeclaration(decl)
724
- ) {
725
- // Additional check: PascalCase convention (first letter uppercase)
726
- // This helps distinguish type references from variables/functions
727
- const firstChar = name.charAt(0);
728
- return firstChar >= 'A' && firstChar <= 'Z';
701
+ if (symbol) {
702
+ const declarations = symbol.getDeclarations();
703
+ if (declarations && declarations.length > 0) {
704
+ sourceFileForLookup = declarations[0].getSourceFile();
729
705
  }
730
706
  }
731
707
 
732
- return false;
733
- }
708
+ const typeInfo = getTypeInfoForGenerator(type, checker, sourceFileForLookup);
709
+ if (!typeInfo) return null;
710
+
711
+ try {
712
+ // Create generator config
713
+ const generatorConfig: any = {
714
+ path: typeInfo.sourceFile,
715
+ tsconfig: configPath || 'tsconfig.json',
716
+ type: typeInfo.typeName,
717
+ expose: 'all',
718
+ jsDoc: 'extended',
719
+ topRef: true,
720
+ skipTypeCheck: true,
721
+ additionalProperties: false,
722
+ };
734
723
 
735
- /**
736
- * Check if a schema is a fallback 'any' (not a real schema)
737
- * Uses type checking instead of string length heuristics
738
- */
739
- function isFallbackAnySchema(
740
- schema: JsonSchemaProperty,
741
- type: ts.Type,
742
- checker: ts.TypeChecker
743
- ): boolean {
744
- // Empty schema {} means "any value" in JSON Schema draft 2020-12
745
- // If schema is empty (no keys), it's a fallback "any" schema
746
- if (Object.keys(schema).length === 0) {
747
- return true;
748
- }
749
-
750
- // If it has properties, anyOf, or items, it's a real schema
751
- if (schema.properties || schema.anyOf || schema.items) {
752
- return false;
753
- }
754
-
755
- // If it has a type property (and it's not 'any'), it's a real schema
756
- if (schema.type && schema.type !== 'any') {
757
- return false;
758
- }
759
-
760
- // Check if it's actually a type reference we couldn't resolve
761
- // (not a primitive, not an object we processed, etc.)
762
- return isTypeReference(type, checker);
763
- }
724
+ const generator = tsJsonSchemaGeneratorModule.createGenerator(generatorConfig);
725
+ const schema = generator.createSchema(typeInfo.typeName);
764
726
 
765
- /**
766
- * Resolve type references (type aliases, interfaces) to their actual types
767
- */
768
- function resolveTypeReference(
769
- type: ts.Type,
770
- checker: ts.TypeChecker
771
- ): ts.Type | null {
772
- const symbol = type.getSymbol();
773
- if (!symbol) return null;
727
+ // Extract $defs if present and merge into our defs
728
+ if (schema.$defs && defs) {
729
+ Object.assign(defs, schema.$defs);
730
+ }
774
731
 
775
- const declarations = symbol.getDeclarations();
776
- if (!declarations || declarations.length === 0) return null;
732
+ // Remove $defs from the schema itself (we'll add it at the top level)
733
+ const { $defs, ...schemaWithoutDefs } = schema as any;
777
734
 
778
- // Try to get the actual type from the declaration
779
- for (const decl of declarations) {
780
- if (ts.isTypeAliasDeclaration(decl) && decl.type) {
781
- // Resolve the type alias
782
- return checker.getTypeFromTypeNode(decl.type);
783
- }
784
- if (ts.isInterfaceDeclaration(decl)) {
785
- // Get the interface type
786
- return checker.getTypeAtLocation(decl);
787
- }
788
- }
735
+ // Handle optional (check if type includes undefined)
736
+ const isOptional = type.flags & ts.TypeFlags.Undefined ||
737
+ (type.isUnion() && type.types.some(t => t.flags & ts.TypeFlags.Undefined));
789
738
 
790
- return null;
739
+ return {
740
+ isOptional: !!isOptional,
741
+ schema: schemaWithoutDefs as JsonSchemaProperty,
742
+ };
743
+ } catch (error) {
744
+ // If generator fails, return null (will result in empty schema)
745
+ return null;
746
+ }
791
747
  }
792
748
 
749
+ // Cache for generator results to avoid repeated calls
750
+ const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
751
+
793
752
  /**
794
- * Serialize a TypeScript type to JSON Schema format
795
- * Returns: { isOptional, schema }
753
+ * 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
796
756
  */
797
- function serializeType(
757
+ async function serializeType(
798
758
  type: ts.Type,
799
759
  checker: ts.TypeChecker,
800
- visited = new Set<number>(),
801
- depth = 0,
760
+ program: ts.Program | undefined,
761
+ configPath: string | undefined,
802
762
  defs?: Record<string, JsonSchemaProperty>,
803
- defsVisited = new Set<number>()
804
- ): { isOptional: boolean; schema: JsonSchemaProperty } {
805
- const typeString = checker.typeToString(type);
806
-
807
- if (depth > MAX_SERIALIZATION_DEPTH) {
808
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
763
+ sourceFileForLookup?: ts.SourceFile
764
+ ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
765
+ if (!program || !configPath) {
766
+ return { isOptional: false, schema: {} }; // Empty schema = any value
809
767
  }
810
768
 
811
- const typeId = getTypeId(type);
812
-
813
- // Handle recursive types with $defs support
814
- if (typeId !== undefined && visited.has(typeId)) {
815
- // Check if this is a named type that we can reference
816
- if (defs) {
817
- const typeName = getTypeNameForDefs(type, checker);
818
- if (typeName) {
819
- // If the definition already exists, use $ref
820
- if (defs[typeName]) {
821
- return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
822
- }
823
- // If we're currently building this definition, use $ref (circular reference)
824
- if (defsVisited.has(typeId)) {
825
- return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
826
- }
827
- }
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];
828
778
  }
829
- // Fallback: empty schema for anonymous recursive types
830
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
831
- }
832
-
833
- // Handle primitives
834
- if (type.flags & ts.TypeFlags.String) {
835
- return { isOptional: false, schema: { type: 'string' } };
836
- }
837
- if (type.flags & ts.TypeFlags.Number) {
838
- return { isOptional: false, schema: { type: 'number' } };
839
- }
840
- if (type.flags & ts.TypeFlags.Boolean) {
841
- return { isOptional: false, schema: { type: 'boolean' } };
842
- }
843
-
844
- // Handle literals using helper function
845
- const literalSchema = serializeLiteralType(type);
846
- if (literalSchema) {
847
- return { isOptional: false, schema: literalSchema };
848
779
  }
849
780
 
850
- if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
851
- return { isOptional: true, schema: { type: 'null' } };
852
- }
853
-
854
- // Handle union types
855
- if (type.isUnion()) {
856
- const types = type.types;
857
-
858
- // Separate null, undefined, and other types
859
- const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
860
- const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
861
- const nonNullUndefinedTypes = types.filter(
862
- t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
863
- );
864
-
865
- // Check if it's a string literal union (enum) - possibly with null/undefined
866
- const stringLiterals = nonNullUndefinedTypes
867
- .map(t => getStringLiteralValue(t))
868
- .filter((v): v is string => v !== null);
869
-
870
- // If all non-null/undefined types are string literals, use enum
871
- if (stringLiterals.length > 0 && stringLiterals.length === nonNullUndefinedTypes.length) {
872
- const enumSchema: JsonSchemaProperty = { type: 'string', enum: stringLiterals };
873
-
874
- // If null or undefined is also present, wrap in anyOf
875
- if (hasNull || hasUndefined) {
876
- return {
877
- isOptional: false,
878
- schema: { anyOf: [enumSchema, { type: 'null' }] }
879
- };
880
- }
881
-
882
- return {
883
- isOptional: false,
884
- schema: enumSchema
885
- };
886
- }
887
-
888
- // Handle optional (T | undefined) - make it optional instead of union
889
- if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
890
- const result = serializeType(nonNullUndefinedTypes[0], checker, visited, depth + 1, defs, defsVisited);
891
- return { isOptional: true, schema: result.schema };
892
- }
893
-
894
- // Build anyOf array for proper JSON Schema union
895
- const anyOf: JsonSchemaProperty[] = [];
896
-
897
- // Add null if present
898
- if (hasNull) {
899
- anyOf.push({ type: 'null' });
900
- }
901
-
902
- // Add undefined as null (JSON doesn't have undefined)
903
- if (hasUndefined) {
904
- anyOf.push({ type: 'null' });
905
- }
906
-
907
- // Process non-null/undefined types
908
- for (const unionType of nonNullUndefinedTypes) {
909
- // Check if it's a primitive type
910
- if (unionType.flags & ts.TypeFlags.String) {
911
- anyOf.push({ type: 'string' });
912
- } else if (unionType.flags & ts.TypeFlags.Number) {
913
- anyOf.push({ type: 'number' });
914
- } else if (unionType.flags & ts.TypeFlags.Boolean) {
915
- anyOf.push({ type: 'boolean' });
916
- } else {
917
- // Try literal types first (using helper function)
918
- const literalSchema = serializeLiteralType(unionType);
919
- if (literalSchema) {
920
- anyOf.push(literalSchema);
921
- } else if (checker.isArrayType(unionType)) {
922
- // Handle array types
923
- const typeArgs = (unionType as ts.TypeReference).typeArguments;
924
- if (typeArgs && typeArgs.length > 0) {
925
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
926
- anyOf.push({ type: 'array', items: itemResult.schema });
927
- } else {
928
- anyOf.push({ type: 'array' });
929
- }
930
- } else {
931
- // Try to serialize the type (handles objects, type references, etc.)
932
- // This will recursively resolve type aliases and interfaces
933
- try {
934
- const refResult = serializeType(unionType, checker, visited, depth + 1, defs, defsVisited);
935
-
936
- // Check if we got a valid schema (not just 'any' fallback)
937
- if (!isFallbackAnySchema(refResult.schema, unionType, checker)) {
938
- anyOf.push(refResult.schema);
939
- } else {
940
- // Try to resolve type reference before giving up
941
- const resolvedType = resolveTypeReference(unionType, checker);
942
- if (resolvedType) {
943
- try {
944
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
945
- if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
946
- anyOf.push(resolvedResult.schema);
947
- } else {
948
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
949
- }
950
- } catch (error) {
951
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
952
- }
953
- } else {
954
- // It's a type reference we couldn't fully resolve - use empty schema (any value)
955
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
956
- }
957
- }
958
- } catch (error) {
959
- // Fallback for unresolvable types
960
- const unionTypeString = checker.typeToString(unionType);
961
- console.warn(`Warning: Could not serialize union type: ${unionTypeString}`, error);
962
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
963
- }
964
- }
965
- }
966
- }
967
-
968
- // If we only have one type (plus null/undefined), simplify
969
- if (anyOf.length === 1) {
970
- return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
971
- }
972
-
973
- // If we have multiple types, use anyOf
974
- if (anyOf.length > 1) {
975
- // Remove duplicate null entries
976
- const uniqueAnyOf = anyOf.filter((schema, index, self) => {
977
- if (schema.type === 'null') {
978
- return index === self.findIndex(s => s.type === 'null');
979
- }
980
- return true;
981
- });
982
-
983
- return {
984
- isOptional: false,
985
- schema: { anyOf: uniqueAnyOf }
986
- };
987
- }
988
-
989
- // Fallback
990
- return {
991
- isOptional: false,
992
- schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
993
- };
781
+ const typeInfo = getTypeInfoForGenerator(type, checker, lookupFile);
782
+ if (!typeInfo) {
783
+ // Anonymous/inline type - can't serialize with generator
784
+ return { isOptional: false, schema: {} }; // Empty schema = any value
994
785
  }
995
786
 
996
- // Handle intersection types (A & B)
997
- if (type.isIntersection()) {
998
- const types = type.types;
999
- const allProperties: Record<string, JsonSchemaProperty> = {};
1000
- const allRequired: string[] = [];
1001
-
1002
- for (const intersectionType of types) {
1003
- const result = serializeType(intersectionType, checker, visited, depth + 1, defs, defsVisited);
1004
- if (result.schema.type === 'object' && result.schema.properties) {
1005
- Object.assign(allProperties, result.schema.properties);
1006
- if (result.schema.required) {
1007
- allRequired.push(...result.schema.required);
1008
- }
1009
- }
1010
- }
1011
-
1012
- if (Object.keys(allProperties).length > 0) {
1013
- return {
1014
- isOptional: false,
1015
- schema: {
1016
- type: 'object',
1017
- properties: allProperties,
1018
- required: Array.from(new Set(allRequired))
1019
- }
1020
- };
1021
- }
787
+ // Check cache first
788
+ const cacheKey = `${typeInfo.sourceFile}:${typeInfo.typeName}`;
789
+ if (generatorCache.has(cacheKey)) {
790
+ return generatorCache.get(cacheKey)!;
1022
791
  }
1023
792
 
1024
- // Handle arrays
1025
- if (checker.isArrayType(type)) {
1026
- const typeArgs = (type as ts.TypeReference).typeArguments;
1027
- if (typeArgs && typeArgs.length > 0) {
1028
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1, defs, defsVisited);
1029
- return {
1030
- isOptional: false,
1031
- schema: { type: 'array', items: itemResult.schema }
1032
- };
1033
- }
1034
- return { isOptional: false, schema: { type: 'array' } };
793
+ // Use generator
794
+ const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
795
+ if (result) {
796
+ generatorCache.set(cacheKey, result);
797
+ return result;
1035
798
  }
1036
799
 
1037
- // Handle objects
1038
- if (type.flags & ts.TypeFlags.Object) {
1039
- if (typeId !== undefined) {
1040
- visited.add(typeId);
1041
- }
1042
-
1043
- const properties: Record<string, JsonSchemaProperty> = {};
1044
- const required: string[] = [];
1045
- const props = checker.getPropertiesOfType(type);
1046
-
1047
- // Check if this is a named type that should go in $defs
1048
- const typeName = defs ? getTypeNameForDefs(type, checker) : null;
1049
- const shouldUseDefs = defs && typeName && props.length > 0;
1050
-
1051
- if (shouldUseDefs && typeId !== undefined) {
1052
- // Check if we're already building this definition
1053
- if (defsVisited.has(typeId)) {
1054
- // Circular reference - return $ref
1055
- return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1056
- }
1057
-
1058
- // Check if definition already exists
1059
- if (defs[typeName]) {
1060
- return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1061
- }
1062
-
1063
- // Start building the definition
1064
- defsVisited.add(typeId);
1065
- // Create placeholder in defs (will be filled below)
1066
- defs[typeName] = { type: 'object', properties: {}, required: [] };
1067
- }
1068
-
1069
- if (props.length > 0) {
1070
- for (const prop of props) {
1071
- try {
1072
- const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
1073
-
1074
- if (!propDeclaration) {
1075
- // Skip properties without declarations
1076
- continue;
1077
- }
1078
-
1079
- const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
1080
- const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
1081
-
1082
- const propResult = serializeType(propType, checker, visited, depth + 1, defs, defsVisited);
1083
- properties[prop.name] = propResult.schema;
1084
-
1085
- // Track required properties
1086
- if (!isOptional && !propResult.isOptional) {
1087
- required.push(prop.name);
1088
- }
1089
- } catch (error) {
1090
- // Skip properties that can't be serialized
1091
- console.warn(`Warning: Could not serialize property ${prop.name}`, error);
1092
- }
1093
- }
1094
-
1095
- // Extract index signatures ([key: string]: T)
1096
- const indexSignatures = checker.getIndexInfosOfType(type);
1097
- if (indexSignatures && indexSignatures.length > 0) {
1098
- try {
1099
- const indexInfo = indexSignatures[0];
1100
- if (indexInfo.declaration) {
1101
- const indexType = checker.getTypeAtLocation(indexInfo.declaration);
1102
- const indexResult = serializeType(indexType, checker, visited, depth + 1, defs, defsVisited);
1103
- // JSON Schema uses additionalProperties for index signatures
1104
- if (indexResult.schema.type !== 'any' || indexResult.schema.properties) {
1105
- // Only set if it's a meaningful type (not just 'any' fallback)
1106
- if (!isFallbackAnySchema(indexResult.schema, indexType, checker)) {
1107
- properties['[key: string]'] = indexResult.schema;
1108
- }
1109
- }
1110
- }
1111
- } catch (error) {
1112
- // Index signature extraction failed, continue without it
1113
- console.warn(`Warning: Could not extract index signature`, error);
1114
- }
1115
- }
1116
-
1117
- const schema: JsonSchemaProperty = {
1118
- type: 'object',
1119
- properties
1120
- };
1121
-
1122
- if (required.length > 0) {
1123
- schema.required = required;
1124
- }
1125
-
1126
- // If we're building a definition, update it and return $ref
1127
- if (shouldUseDefs && typeName && defs) {
1128
- defs[typeName] = schema;
1129
- defsVisited.delete(typeId!);
1130
- return { isOptional: false, schema: { $ref: `#/$defs/${typeName}` } };
1131
- }
1132
-
1133
- return { isOptional: false, schema };
1134
- }
1135
- }
1136
-
1137
- // Fallback - check type flags to determine if it's a valid JSON Schema type
1138
- // Valid JSON Schema types: string, number, integer, boolean, object, array, null
1139
-
1140
- // Check if it's an enum type (TypeScript enum)
1141
- if (type.flags & ts.TypeFlags.Enum) {
1142
- try {
1143
- const symbol = type.getSymbol();
1144
- if (symbol) {
1145
- const enumMembers = checker.getPropertiesOfType(type);
1146
- if (enumMembers.length > 0) {
1147
- // Extract enum values - enum members are typically string or number literals
1148
- const values: (string | number)[] = [];
1149
- for (const member of enumMembers) {
1150
- const memberDeclaration = member.valueDeclaration || member.declarations?.[0];
1151
- if (memberDeclaration) {
1152
- const memberType = checker.getTypeOfSymbolAtLocation(member, memberDeclaration);
1153
- const strValue = getStringLiteralValue(memberType);
1154
- if (strValue !== null) {
1155
- values.push(strValue);
1156
- } else {
1157
- const numValue = getNumberLiteralValue(memberType);
1158
- if (numValue !== null) {
1159
- values.push(numValue);
1160
- }
1161
- }
1162
- }
1163
- }
1164
- if (values.length > 0) {
1165
- // Check if all values are strings or all are numbers
1166
- const allStrings = values.every(v => typeof v === 'string');
1167
- const allNumbers = values.every(v => typeof v === 'number');
1168
- if (allStrings) {
1169
- return { isOptional: false, schema: { type: 'string', enum: values.filter((v): v is string => typeof v === 'string') } };
1170
- } else if (allNumbers) {
1171
- const numberValues = values.filter((v): v is number => typeof v === 'number');
1172
- return { isOptional: false, schema: { type: 'number', enum: numberValues } };
1173
- }
1174
- }
1175
- }
1176
- }
1177
- } catch (error) {
1178
- console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
1179
- }
1180
- // If we can't extract enum values, use empty schema (any value)
1181
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1182
- }
1183
-
1184
- // Check if it's a type reference (like InternalFilterType.StringFilter)
1185
- // Type references that aren't objects/arrays/primitives should use 'any'
1186
- if (type.flags & ts.TypeFlags.Object) {
1187
- // Already handled above, but check if it's an empty object type
1188
- const props = checker.getPropertiesOfType(type);
1189
- if (props.length === 0) {
1190
- // Empty object or type reference we can't resolve
1191
- // Check if typeString suggests it's a named type reference
1192
- if (typeString && typeString.includes('.')) {
1193
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1194
- }
1195
- }
1196
- }
1197
-
1198
- // Check if typeString is a valid JSON Schema primitive type
1199
- const lowerTypeString = typeString?.toLowerCase();
1200
-
1201
- // If it's a valid JSON Schema type, use it (shouldn't happen here, but safety check)
1202
- if (lowerTypeString && VALID_JSON_SCHEMA_TYPES.has(lowerTypeString)) {
1203
- // Type assertion is safe here because VALID_JSON_SCHEMA_TYPES validates the value
1204
- const validType = lowerTypeString as 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
1205
- return {
1206
- isOptional: false,
1207
- schema: { type: validType }
1208
- };
1209
- }
1210
-
1211
- // Try to resolve type references before giving up
1212
- const resolvedType = resolveTypeReference(type, checker);
1213
- if (resolvedType) {
1214
- try {
1215
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1, defs, defsVisited);
1216
- if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
1217
- return resolvedResult;
1218
- }
1219
- } catch (error) {
1220
- // Resolution failed, continue to fallback
1221
- }
1222
- }
1223
-
1224
- // Handle special type flags
1225
- if (type.flags & ts.TypeFlags.Never) {
1226
- return { isOptional: false, schema: { type: 'null' } };
1227
- }
1228
- if (type.flags & ts.TypeFlags.Unknown) {
1229
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1230
- }
1231
- if (type.flags & ts.TypeFlags.Any) {
1232
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1233
- }
1234
-
1235
- // For any other unrecognized type, use empty schema instead of invalid type strings
1236
- // This prevents errors like "type": "InternalFilterType.StringFilter"
1237
- // Empty schema {} means "allow any value" in JSON Schema draft 2020-12
1238
- return {
1239
- isOptional: false,
1240
- schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
1241
- };
800
+ // Generator failed - return empty schema
801
+ return { isOptional: false, schema: {} }; // Empty schema = any value
1242
802
  }
1243
803
 
1244
804
  /**
@@ -1310,11 +870,13 @@ function extractDefaultValue(
1310
870
  /**
1311
871
  * Extract metadata from a function
1312
872
  */
1313
- function extractFunctionMetadata(
873
+ async function extractFunctionMetadata(
1314
874
  node: ts.FunctionDeclaration | ts.VariableDeclaration,
1315
875
  checker: ts.TypeChecker,
1316
- sourceFile: ts.SourceFile
1317
- ): FunctionMetadata | null {
876
+ sourceFile: ts.SourceFile,
877
+ program?: ts.Program,
878
+ configPath?: string
879
+ ): Promise<FunctionMetadata | null> {
1318
880
  let functionNode: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null = null;
1319
881
  let functionName: string;
1320
882
 
@@ -1435,10 +997,10 @@ function extractFunctionMetadata(
1435
997
  }
1436
998
  }
1437
999
 
1438
- // Use a fresh visited set for each parameter to avoid false cycle detection
1439
- // but share defs and defsVisited so recursive types are defined once
1440
- const visited = new Set<number>();
1441
- const result = serializeType(type, checker, visited, 0, defs, defsVisited);
1000
+ // Use ts-json-schema-generator for named types only
1001
+ // 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);
1442
1004
  propSchema = result.schema;
1443
1005
  isOptional = result.isOptional;
1444
1006
  } else {
@@ -1579,7 +1141,7 @@ async function autoDiscoverAndExtract(projectRoot: string, outputPath: string) {
1579
1141
  if (sourceFile) {
1580
1142
  const result = findFunctionDefinition(toolName, sourceFile, program);
1581
1143
  if (result) {
1582
- const metadata = extractFunctionMetadata(result.node, checker, result.sourceFile);
1144
+ const metadata = await extractFunctionMetadata(result.node, checker, result.sourceFile, program, configPath);
1583
1145
  if (metadata) {
1584
1146
  functionsMap[toolName] = metadata;
1585
1147
  discoveredFrom.push(path.relative(projectRoot, result.sourceFile.fileName));