@arcteninc/core 0.0.46 → 0.0.47

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.47",
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,237 +572,36 @@ 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
575
  /**
597
- * Get number literal value from a TypeScript type
576
+ * Try to get type name and source file for use with ts-json-schema-generator
598
577
  */
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
- /**
608
- * Get string literal value from a TypeScript type
609
- */
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;
614
- }
615
- return null;
616
- }
617
-
618
- /**
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
635
- */
636
- function getTypeNameForDefs(
578
+ function getTypeInfoForGenerator(
637
579
  type: ts.Type,
638
580
  checker: ts.TypeChecker
639
- ): string | null {
581
+ ): { typeName: string; sourceFile: string } | null {
640
582
  const symbol = type.getSymbol();
641
- if (symbol) {
642
- const name = symbol.getName();
643
- if (name && name.length > 0) {
644
- // Check if it's a named type (not a primitive)
645
- const declarations = symbol.getDeclarations();
646
- if (declarations && declarations.length > 0) {
647
- // Check if it's a type alias, interface, or class
648
- for (const decl of declarations) {
649
- if (
650
- ts.isTypeAliasDeclaration(decl) ||
651
- ts.isInterfaceDeclaration(decl) ||
652
- ts.isClassDeclaration(decl)
653
- ) {
654
- return name;
655
- }
656
- }
657
- }
658
- }
659
- }
660
-
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;
668
- }
669
-
670
- return null;
671
- }
672
-
673
- /**
674
- * Serialize a literal type (boolean, number, or string literal) to JSON Schema
675
- * Returns null if the type is not a literal
676
- */
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 };
691
- }
692
-
693
- return null;
694
- }
583
+ if (!symbol) return null;
695
584
 
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 {
701
- const symbol = type.getSymbol();
702
- if (!symbol) return false;
703
-
704
585
  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)
586
+ if (!name || name.length === 0) return null;
587
+
713
588
  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
589
+ if (!declarations || declarations.length === 0) return null;
590
+
591
+ // Find the first declaration that's a type alias, interface, or class
718
592
  for (const decl of declarations) {
719
593
  if (
720
594
  ts.isTypeAliasDeclaration(decl) ||
721
595
  ts.isInterfaceDeclaration(decl) ||
722
- ts.isClassDeclaration(decl) ||
723
- ts.isEnumDeclaration(decl)
596
+ ts.isClassDeclaration(decl)
724
597
  ) {
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';
729
- }
730
- }
731
-
732
- return false;
733
- }
734
-
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
- }
764
-
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;
774
-
775
- const declarations = symbol.getDeclarations();
776
- if (!declarations || declarations.length === 0) return null;
777
-
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);
598
+ const sourceFile = decl.getSourceFile();
599
+ if (sourceFile) {
600
+ return {
601
+ typeName: name,
602
+ sourceFile: sourceFile.fileName,
603
+ };
604
+ }
787
605
  }
788
606
  }
789
607
 
@@ -791,454 +609,104 @@ function resolveTypeReference(
791
609
  }
792
610
 
793
611
  /**
794
- * Serialize a TypeScript type to JSON Schema format
795
- * Returns: { isOptional, schema }
612
+ * Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
613
+ * Returns: { isOptional, schema } or null if generator not available
614
+ * Note: This is async and should only be called at top level
796
615
  */
797
- function serializeType(
616
+ async function serializeTypeWithGenerator(
798
617
  type: ts.Type,
799
618
  checker: ts.TypeChecker,
800
- visited = new Set<number>(),
801
- depth = 0,
802
- 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)
809
- }
810
-
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
- }
828
- }
829
- // Fallback: empty schema for anonymous recursive types
830
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
619
+ program: ts.Program,
620
+ configPath: string | undefined,
621
+ defs?: Record<string, JsonSchemaProperty>
622
+ ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty } | null> {
623
+ // Try to load generator if not already loaded
624
+ const generatorLoaded = await ensureGeneratorLoaded();
625
+ if (!generatorLoaded || !tsJsonSchemaGeneratorModule) {
626
+ return null;
831
627
  }
832
628
 
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
- }
849
-
850
- if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
851
- return { isOptional: true, schema: { type: 'null' } };
852
- }
629
+ const typeInfo = getTypeInfoForGenerator(type, checker);
630
+ if (!typeInfo) return null;
631
+
632
+ try {
633
+ // Create generator config
634
+ const generatorConfig: any = {
635
+ path: typeInfo.sourceFile,
636
+ tsconfig: configPath || 'tsconfig.json',
637
+ type: typeInfo.typeName,
638
+ expose: 'all',
639
+ jsDoc: 'extended',
640
+ topRef: true,
641
+ skipTypeCheck: true,
642
+ additionalProperties: false,
643
+ };
853
644
 
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
- }
645
+ const generator = tsJsonSchemaGeneratorModule.createGenerator(generatorConfig);
646
+ const schema = generator.createSchema(typeInfo.typeName);
887
647
 
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 };
648
+ // Extract $defs if present and merge into our defs
649
+ if (schema.$defs && defs) {
650
+ Object.assign(defs, schema.$defs);
892
651
  }
893
652
 
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
- }
653
+ // Remove $defs from the schema itself (we'll add it at the top level)
654
+ const { $defs, ...schemaWithoutDefs } = schema as any;
901
655
 
902
- // Add undefined as null (JSON doesn't have undefined)
903
- if (hasUndefined) {
904
- anyOf.push({ type: 'null' });
905
- }
656
+ // Handle optional (check if type includes undefined)
657
+ const isOptional = type.flags & ts.TypeFlags.Undefined ||
658
+ (type.isUnion() && type.types.some(t => t.flags & ts.TypeFlags.Undefined));
906
659
 
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
660
  return {
991
- isOptional: false,
992
- schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
661
+ isOptional: !!isOptional,
662
+ schema: schemaWithoutDefs as JsonSchemaProperty,
993
663
  };
664
+ } catch (error) {
665
+ // If generator fails, return null (will result in empty schema)
666
+ return null;
994
667
  }
668
+ }
995
669
 
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
- }
670
+ // Cache for generator results to avoid repeated calls
671
+ const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
1011
672
 
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
- }
673
+ /**
674
+ * Serialize a TypeScript type to JSON Schema format using ts-json-schema-generator
675
+ * Only works for named types (interfaces, type aliases, classes)
676
+ * Returns empty schema for anonymous types
677
+ */
678
+ async function serializeType(
679
+ type: ts.Type,
680
+ checker: ts.TypeChecker,
681
+ program: ts.Program | undefined,
682
+ configPath: string | undefined,
683
+ defs?: Record<string, JsonSchemaProperty>
684
+ ): Promise<{ isOptional: boolean; schema: JsonSchemaProperty }> {
685
+ if (!program || !configPath) {
686
+ return { isOptional: false, schema: {} }; // Empty schema = any value
1022
687
  }
1023
688
 
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' } };
689
+ const typeInfo = getTypeInfoForGenerator(type, checker);
690
+ if (!typeInfo) {
691
+ // Anonymous/inline type - can't serialize with generator
692
+ return { isOptional: false, schema: {} }; // Empty schema = any value
1035
693
  }
1036
694
 
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
- }
695
+ // Check cache first
696
+ const cacheKey = `${typeInfo.sourceFile}:${typeInfo.typeName}`;
697
+ if (generatorCache.has(cacheKey)) {
698
+ return generatorCache.get(cacheKey)!;
1222
699
  }
1223
700
 
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)
701
+ // Use generator
702
+ const result = await serializeTypeWithGenerator(type, checker, program, configPath, defs);
703
+ if (result) {
704
+ generatorCache.set(cacheKey, result);
705
+ return result;
1233
706
  }
1234
707
 
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
- };
708
+ // Generator failed - return empty schema
709
+ return { isOptional: false, schema: {} }; // Empty schema = any value
1242
710
  }
1243
711
 
1244
712
  /**
@@ -1310,11 +778,13 @@ function extractDefaultValue(
1310
778
  /**
1311
779
  * Extract metadata from a function
1312
780
  */
1313
- function extractFunctionMetadata(
781
+ async function extractFunctionMetadata(
1314
782
  node: ts.FunctionDeclaration | ts.VariableDeclaration,
1315
783
  checker: ts.TypeChecker,
1316
- sourceFile: ts.SourceFile
1317
- ): FunctionMetadata | null {
784
+ sourceFile: ts.SourceFile,
785
+ program?: ts.Program,
786
+ configPath?: string
787
+ ): Promise<FunctionMetadata | null> {
1318
788
  let functionNode: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null = null;
1319
789
  let functionName: string;
1320
790
 
@@ -1435,10 +905,9 @@ function extractFunctionMetadata(
1435
905
  }
1436
906
  }
1437
907
 
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);
908
+ // Use ts-json-schema-generator for named types only
909
+ // Anonymous types are not supported - agents can't use them anyway
910
+ const result = await serializeType(type, checker, program, configPath, defs);
1442
911
  propSchema = result.schema;
1443
912
  isOptional = result.isOptional;
1444
913
  } else {
@@ -1579,7 +1048,7 @@ async function autoDiscoverAndExtract(projectRoot: string, outputPath: string) {
1579
1048
  if (sourceFile) {
1580
1049
  const result = findFunctionDefinition(toolName, sourceFile, program);
1581
1050
  if (result) {
1582
- const metadata = extractFunctionMetadata(result.node, checker, result.sourceFile);
1051
+ const metadata = await extractFunctionMetadata(result.node, checker, result.sourceFile, program, configPath);
1583
1052
  if (metadata) {
1584
1053
  functionsMap[toolName] = metadata;
1585
1054
  discoveredFrom.push(path.relative(projectRoot, result.sourceFile.fileName));