@arcteninc/core 0.0.45 → 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.45",
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
@@ -33,6 +52,7 @@ interface FunctionMetadata {
33
52
  type: 'object';
34
53
  properties: Record<string, JsonSchemaProperty>;
35
54
  required?: string[]; // Array of required parameter names
55
+ $defs?: Record<string, JsonSchemaProperty>; // Type definitions for recursive types
36
56
  };
37
57
  returnType?: string;
38
58
  isAsync?: boolean;
@@ -552,195 +572,36 @@ function resolveImportPath(
552
572
  return null;
553
573
  }
554
574
 
555
- // Constants for JSON Schema validation
556
- // These are actual constants from the JSON Schema specification
557
- const VALID_JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
558
-
559
- // Safety limit for serialization depth (only used as a last resort)
560
- // Note: Cycle detection via 'visited' Set is the primary protection against infinite recursion
561
- // This is only a fallback safety net for edge cases
562
- const MAX_SERIALIZATION_DEPTH = 50; // Increased from 10 - legitimate types can be deeply nested
563
-
564
- // Note: MAX_TOOL_NAME_LENGTH was removed - we now use proper AST node type checks
565
- // instead of string length heuristics for better accuracy
566
-
567
- /**
568
- * Type-safe accessors for TypeScript internal APIs
569
- * These use type guards to safely access properties that aren't in the public API
570
- */
571
-
572
- interface BooleanLiteralType extends ts.Type {
573
- intrinsicName: 'true' | 'false';
574
- }
575
-
576
- interface NumberLiteralType extends ts.Type {
577
- value: number;
578
- }
579
-
580
- interface StringLiteralType extends ts.Type {
581
- value: string;
582
- }
583
-
584
- /**
585
- * Get boolean literal value from a TypeScript type
586
- */
587
- function getBooleanLiteralValue(type: ts.Type): boolean | null {
588
- if (type.flags & ts.TypeFlags.BooleanLiteral) {
589
- const boolType = type as BooleanLiteralType;
590
- return boolType.intrinsicName === 'true';
591
- }
592
- return null;
593
- }
594
-
595
- /**
596
- * Get number literal value from a TypeScript type
597
- */
598
- function getNumberLiteralValue(type: ts.Type): number | null {
599
- if (type.flags & ts.TypeFlags.NumberLiteral) {
600
- const numType = type as NumberLiteralType;
601
- return numType.value;
602
- }
603
- return null;
604
- }
605
-
606
- /**
607
- * Get string literal value from a TypeScript type
608
- */
609
- function getStringLiteralValue(type: ts.Type): string | null {
610
- if (type.flags & ts.TypeFlags.StringLiteral) {
611
- const strType = type as StringLiteralType;
612
- return strType.value;
613
- }
614
- return null;
615
- }
616
-
617
- /**
618
- * Get type ID safely (for cycle detection)
619
- * Uses TypeScript's internal API - this is necessary for cycle detection
620
- * but we wrap it in a function to centralize the unsafe access
621
- */
622
- function getTypeId(type: ts.Type): number | undefined {
623
- // TypeScript's Type interface doesn't expose 'id' in public API,
624
- // but it's available internally and needed for cycle detection.
625
- // This is a known limitation when working with TypeScript's compiler API.
626
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
627
- return (type as any).id;
628
- }
629
-
630
575
  /**
631
- * Serialize a literal type (boolean, number, or string literal) to JSON Schema
632
- * Returns null if the type is not a literal
576
+ * Try to get type name and source file for use with ts-json-schema-generator
633
577
  */
634
- function serializeLiteralType(type: ts.Type): JsonSchemaProperty | null {
635
- const boolValue = getBooleanLiteralValue(type);
636
- if (boolValue !== null) {
637
- return { type: 'boolean', const: boolValue };
638
- }
639
-
640
- const numValue = getNumberLiteralValue(type);
641
- if (numValue !== null) {
642
- return { type: 'number', const: numValue };
643
- }
644
-
645
- const strValue = getStringLiteralValue(type);
646
- if (strValue !== null) {
647
- return { type: 'string', const: strValue };
648
- }
649
-
650
- return null;
651
- }
652
-
653
- /**
654
- * Check if a type is a type reference (named type like FilterTreeNode)
655
- * Uses TypeScript API to determine if it's a named type reference
656
- */
657
- function isTypeReference(type: ts.Type, checker: ts.TypeChecker): boolean {
658
- const symbol = type.getSymbol();
659
- if (!symbol) return false;
660
-
661
- const name = symbol.getName();
662
- if (name.length === 0) return false;
663
-
664
- // Check if it's a primitive JSON Schema type (should not be treated as type reference)
665
- if (VALID_JSON_SCHEMA_TYPES.has(name.toLowerCase())) {
666
- return false;
667
- }
668
-
669
- // Check if it's a named type (has declarations and is not a primitive)
670
- const declarations = symbol.getDeclarations();
671
- if (!declarations || declarations.length === 0) return false;
672
-
673
- // Check if any declaration is a type alias, interface, or class
674
- // This is more reliable than regex pattern matching
675
- for (const decl of declarations) {
676
- if (
677
- ts.isTypeAliasDeclaration(decl) ||
678
- ts.isInterfaceDeclaration(decl) ||
679
- ts.isClassDeclaration(decl) ||
680
- ts.isEnumDeclaration(decl)
681
- ) {
682
- // Additional check: PascalCase convention (first letter uppercase)
683
- // This helps distinguish type references from variables/functions
684
- const firstChar = name.charAt(0);
685
- return firstChar >= 'A' && firstChar <= 'Z';
686
- }
687
- }
688
-
689
- return false;
690
- }
691
-
692
- /**
693
- * Check if a schema is a fallback 'any' (not a real schema)
694
- * Uses type checking instead of string length heuristics
695
- */
696
- function isFallbackAnySchema(
697
- schema: JsonSchemaProperty,
578
+ function getTypeInfoForGenerator(
698
579
  type: ts.Type,
699
580
  checker: ts.TypeChecker
700
- ): boolean {
701
- // Empty schema {} means "any value" in JSON Schema draft 2020-12
702
- // If schema is empty (no keys), it's a fallback "any" schema
703
- if (Object.keys(schema).length === 0) {
704
- return true;
705
- }
706
-
707
- // If it has properties, anyOf, or items, it's a real schema
708
- if (schema.properties || schema.anyOf || schema.items) {
709
- return false;
710
- }
711
-
712
- // If it has a type property (and it's not 'any'), it's a real schema
713
- if (schema.type && schema.type !== 'any') {
714
- return false;
715
- }
716
-
717
- // Check if it's actually a type reference we couldn't resolve
718
- // (not a primitive, not an object we processed, etc.)
719
- return isTypeReference(type, checker);
720
- }
721
-
722
- /**
723
- * Resolve type references (type aliases, interfaces) to their actual types
724
- */
725
- function resolveTypeReference(
726
- type: ts.Type,
727
- checker: ts.TypeChecker
728
- ): ts.Type | null {
581
+ ): { typeName: string; sourceFile: string } | null {
729
582
  const symbol = type.getSymbol();
730
583
  if (!symbol) return null;
731
584
 
585
+ const name = symbol.getName();
586
+ if (!name || name.length === 0) return null;
587
+
732
588
  const declarations = symbol.getDeclarations();
733
589
  if (!declarations || declarations.length === 0) return null;
734
590
 
735
- // Try to get the actual type from the declaration
591
+ // Find the first declaration that's a type alias, interface, or class
736
592
  for (const decl of declarations) {
737
- if (ts.isTypeAliasDeclaration(decl) && decl.type) {
738
- // Resolve the type alias
739
- return checker.getTypeFromTypeNode(decl.type);
740
- }
741
- if (ts.isInterfaceDeclaration(decl)) {
742
- // Get the interface type
743
- return checker.getTypeAtLocation(decl);
593
+ if (
594
+ ts.isTypeAliasDeclaration(decl) ||
595
+ ts.isInterfaceDeclaration(decl) ||
596
+ ts.isClassDeclaration(decl)
597
+ ) {
598
+ const sourceFile = decl.getSourceFile();
599
+ if (sourceFile) {
600
+ return {
601
+ typeName: name,
602
+ sourceFile: sourceFile.fileName,
603
+ };
604
+ }
744
605
  }
745
606
  }
746
607
 
@@ -748,406 +609,104 @@ function resolveTypeReference(
748
609
  }
749
610
 
750
611
  /**
751
- * Serialize a TypeScript type to JSON Schema format
752
- * 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
753
615
  */
754
- function serializeType(
616
+ async function serializeTypeWithGenerator(
755
617
  type: ts.Type,
756
618
  checker: ts.TypeChecker,
757
- visited = new Set<number>(),
758
- depth = 0
759
- ): { isOptional: boolean; schema: JsonSchemaProperty } {
760
- const typeString = checker.typeToString(type);
761
-
762
- if (depth > MAX_SERIALIZATION_DEPTH) {
763
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
764
- }
765
-
766
- const typeId = getTypeId(type);
767
- if (typeId !== undefined && visited.has(typeId)) {
768
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
769
- }
770
-
771
- // Handle primitives
772
- if (type.flags & ts.TypeFlags.String) {
773
- return { isOptional: false, schema: { type: 'string' } };
774
- }
775
- if (type.flags & ts.TypeFlags.Number) {
776
- return { isOptional: false, schema: { type: 'number' } };
777
- }
778
- if (type.flags & ts.TypeFlags.Boolean) {
779
- return { isOptional: false, schema: { type: 'boolean' } };
780
- }
781
-
782
- // Handle literals using helper function
783
- const literalSchema = serializeLiteralType(type);
784
- if (literalSchema) {
785
- return { isOptional: false, schema: literalSchema };
786
- }
787
-
788
- if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
789
- return { isOptional: true, schema: { type: 'null' } };
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;
790
627
  }
791
628
 
792
- // Handle union types
793
- if (type.isUnion()) {
794
- const types = type.types;
795
-
796
- // Separate null, undefined, and other types
797
- const hasNull = types.some(t => t.flags & ts.TypeFlags.Null);
798
- const hasUndefined = types.some(t => t.flags & ts.TypeFlags.Undefined);
799
- const nonNullUndefinedTypes = types.filter(
800
- t => !(t.flags & ts.TypeFlags.Null) && !(t.flags & ts.TypeFlags.Undefined)
801
- );
802
-
803
- // Check if it's a string literal union (enum) - possibly with null/undefined
804
- const stringLiterals = nonNullUndefinedTypes
805
- .map(t => getStringLiteralValue(t))
806
- .filter((v): v is string => v !== null);
807
-
808
- // If all non-null/undefined types are string literals, use enum
809
- if (stringLiterals.length > 0 && stringLiterals.length === nonNullUndefinedTypes.length) {
810
- const enumSchema: JsonSchemaProperty = { type: 'string', enum: stringLiterals };
811
-
812
- // If null or undefined is also present, wrap in anyOf
813
- if (hasNull || hasUndefined) {
814
- return {
815
- isOptional: false,
816
- schema: { anyOf: [enumSchema, { type: 'null' }] }
817
- };
818
- }
819
-
820
- return {
821
- isOptional: false,
822
- schema: enumSchema
823
- };
824
- }
825
-
826
- // Handle optional (T | undefined) - make it optional instead of union
827
- if (hasUndefined && nonNullUndefinedTypes.length === 1 && !hasNull) {
828
- const result = serializeType(nonNullUndefinedTypes[0], checker, visited, depth + 1);
829
- return { isOptional: true, schema: result.schema };
830
- }
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
+ };
831
644
 
832
- // Build anyOf array for proper JSON Schema union
833
- const anyOf: JsonSchemaProperty[] = [];
645
+ const generator = tsJsonSchemaGeneratorModule.createGenerator(generatorConfig);
646
+ const schema = generator.createSchema(typeInfo.typeName);
834
647
 
835
- // Add null if present
836
- if (hasNull) {
837
- anyOf.push({ type: 'null' });
648
+ // Extract $defs if present and merge into our defs
649
+ if (schema.$defs && defs) {
650
+ Object.assign(defs, schema.$defs);
838
651
  }
839
652
 
840
- // Add undefined as null (JSON doesn't have undefined)
841
- if (hasUndefined) {
842
- anyOf.push({ type: 'null' });
843
- }
653
+ // Remove $defs from the schema itself (we'll add it at the top level)
654
+ const { $defs, ...schemaWithoutDefs } = schema as any;
844
655
 
845
- // Process non-null/undefined types
846
- for (const unionType of nonNullUndefinedTypes) {
847
- // Check if it's a primitive type
848
- if (unionType.flags & ts.TypeFlags.String) {
849
- anyOf.push({ type: 'string' });
850
- } else if (unionType.flags & ts.TypeFlags.Number) {
851
- anyOf.push({ type: 'number' });
852
- } else if (unionType.flags & ts.TypeFlags.Boolean) {
853
- anyOf.push({ type: 'boolean' });
854
- } else {
855
- // Try literal types first (using helper function)
856
- const literalSchema = serializeLiteralType(unionType);
857
- if (literalSchema) {
858
- anyOf.push(literalSchema);
859
- } else if (checker.isArrayType(unionType)) {
860
- // Handle array types
861
- const typeArgs = (unionType as ts.TypeReference).typeArguments;
862
- if (typeArgs && typeArgs.length > 0) {
863
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1);
864
- anyOf.push({ type: 'array', items: itemResult.schema });
865
- } else {
866
- anyOf.push({ type: 'array' });
867
- }
868
- } else {
869
- // Try to serialize the type (handles objects, type references, etc.)
870
- // This will recursively resolve type aliases and interfaces
871
- try {
872
- const refResult = serializeType(unionType, checker, visited, depth + 1);
873
-
874
- // Check if we got a valid schema (not just 'any' fallback)
875
- if (!isFallbackAnySchema(refResult.schema, unionType, checker)) {
876
- anyOf.push(refResult.schema);
877
- } else {
878
- // Try to resolve type reference before giving up
879
- const resolvedType = resolveTypeReference(unionType, checker);
880
- if (resolvedType) {
881
- try {
882
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
883
- if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
884
- anyOf.push(resolvedResult.schema);
885
- } else {
886
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
887
- }
888
- } catch (error) {
889
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
890
- }
891
- } else {
892
- // It's a type reference we couldn't fully resolve - use empty schema (any value)
893
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
894
- }
895
- }
896
- } catch (error) {
897
- // Fallback for unresolvable types
898
- const unionTypeString = checker.typeToString(unionType);
899
- console.warn(`Warning: Could not serialize union type: ${unionTypeString}`, error);
900
- anyOf.push({}); // Empty schema = any value (JSON Schema draft 2020-12)
901
- }
902
- }
903
- }
904
- }
905
-
906
- // If we only have one type (plus null/undefined), simplify
907
- if (anyOf.length === 1) {
908
- return { isOptional: hasNull || hasUndefined, schema: anyOf[0] };
909
- }
910
-
911
- // If we have multiple types, use anyOf
912
- if (anyOf.length > 1) {
913
- // Remove duplicate null entries
914
- const uniqueAnyOf = anyOf.filter((schema, index, self) => {
915
- if (schema.type === 'null') {
916
- return index === self.findIndex(s => s.type === 'null');
917
- }
918
- return true;
919
- });
920
-
921
- return {
922
- isOptional: false,
923
- schema: { anyOf: uniqueAnyOf }
924
- };
925
- }
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));
926
659
 
927
- // Fallback
928
660
  return {
929
- isOptional: false,
930
- schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
661
+ isOptional: !!isOptional,
662
+ schema: schemaWithoutDefs as JsonSchemaProperty,
931
663
  };
664
+ } catch (error) {
665
+ // If generator fails, return null (will result in empty schema)
666
+ return null;
932
667
  }
668
+ }
933
669
 
934
- // Handle intersection types (A & B)
935
- if (type.isIntersection()) {
936
- const types = type.types;
937
- const allProperties: Record<string, JsonSchemaProperty> = {};
938
- const allRequired: string[] = [];
939
-
940
- for (const intersectionType of types) {
941
- const result = serializeType(intersectionType, checker, visited, depth + 1);
942
- if (result.schema.type === 'object' && result.schema.properties) {
943
- Object.assign(allProperties, result.schema.properties);
944
- if (result.schema.required) {
945
- allRequired.push(...result.schema.required);
946
- }
947
- }
948
- }
949
-
950
- if (Object.keys(allProperties).length > 0) {
951
- return {
952
- isOptional: false,
953
- schema: {
954
- type: 'object',
955
- properties: allProperties,
956
- required: Array.from(new Set(allRequired))
957
- }
958
- };
959
- }
960
- }
670
+ // Cache for generator results to avoid repeated calls
671
+ const generatorCache = new Map<string, { isOptional: boolean; schema: JsonSchemaProperty }>();
961
672
 
962
- // Handle arrays
963
- if (checker.isArrayType(type)) {
964
- const typeArgs = (type as ts.TypeReference).typeArguments;
965
- if (typeArgs && typeArgs.length > 0) {
966
- const itemResult = serializeType(typeArgs[0], checker, visited, depth + 1);
967
- return {
968
- isOptional: false,
969
- schema: { type: 'array', items: itemResult.schema }
970
- };
971
- }
972
- return { isOptional: false, schema: { type: 'array' } };
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
973
687
  }
974
688
 
975
- // Handle objects
976
- if (type.flags & ts.TypeFlags.Object) {
977
- if (typeId !== undefined) {
978
- visited.add(typeId);
979
- }
980
-
981
- const properties: Record<string, JsonSchemaProperty> = {};
982
- const required: string[] = [];
983
- const props = checker.getPropertiesOfType(type);
984
-
985
- if (props.length > 0) {
986
- for (const prop of props) {
987
- try {
988
- const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
989
-
990
- if (!propDeclaration) {
991
- // Skip properties without declarations
992
- continue;
993
- }
994
-
995
- const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
996
- const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
997
-
998
- const propResult = serializeType(propType, checker, visited, depth + 1);
999
- properties[prop.name] = propResult.schema;
1000
-
1001
- // Track required properties
1002
- if (!isOptional && !propResult.isOptional) {
1003
- required.push(prop.name);
1004
- }
1005
- } catch (error) {
1006
- // Skip properties that can't be serialized
1007
- console.warn(`Warning: Could not serialize property ${prop.name}`, error);
1008
- }
1009
- }
1010
-
1011
- // Extract index signatures ([key: string]: T)
1012
- const indexSignatures = checker.getIndexInfosOfType(type);
1013
- if (indexSignatures && indexSignatures.length > 0) {
1014
- try {
1015
- const indexInfo = indexSignatures[0];
1016
- if (indexInfo.declaration) {
1017
- const indexType = checker.getTypeAtLocation(indexInfo.declaration);
1018
- const indexResult = serializeType(indexType, checker, visited, depth + 1);
1019
- // JSON Schema uses additionalProperties for index signatures
1020
- if (indexResult.schema.type !== 'any' || indexResult.schema.properties) {
1021
- // Only set if it's a meaningful type (not just 'any' fallback)
1022
- if (!isFallbackAnySchema(indexResult.schema, indexType, checker)) {
1023
- properties['[key: string]'] = indexResult.schema;
1024
- }
1025
- }
1026
- }
1027
- } catch (error) {
1028
- // Index signature extraction failed, continue without it
1029
- console.warn(`Warning: Could not extract index signature`, error);
1030
- }
1031
- }
1032
-
1033
- const schema: JsonSchemaProperty = {
1034
- type: 'object',
1035
- properties
1036
- };
1037
-
1038
- if (required.length > 0) {
1039
- schema.required = required;
1040
- }
1041
-
1042
- return { isOptional: false, schema };
1043
- }
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
1044
693
  }
1045
694
 
1046
- // Fallback - check type flags to determine if it's a valid JSON Schema type
1047
- // Valid JSON Schema types: string, number, integer, boolean, object, array, null
1048
-
1049
- // Check if it's an enum type (TypeScript enum)
1050
- if (type.flags & ts.TypeFlags.Enum) {
1051
- try {
1052
- const symbol = type.getSymbol();
1053
- if (symbol) {
1054
- const enumMembers = checker.getPropertiesOfType(type);
1055
- if (enumMembers.length > 0) {
1056
- // Extract enum values - enum members are typically string or number literals
1057
- const values: (string | number)[] = [];
1058
- for (const member of enumMembers) {
1059
- const memberDeclaration = member.valueDeclaration || member.declarations?.[0];
1060
- if (memberDeclaration) {
1061
- const memberType = checker.getTypeOfSymbolAtLocation(member, memberDeclaration);
1062
- const strValue = getStringLiteralValue(memberType);
1063
- if (strValue !== null) {
1064
- values.push(strValue);
1065
- } else {
1066
- const numValue = getNumberLiteralValue(memberType);
1067
- if (numValue !== null) {
1068
- values.push(numValue);
1069
- }
1070
- }
1071
- }
1072
- }
1073
- if (values.length > 0) {
1074
- // Check if all values are strings or all are numbers
1075
- const allStrings = values.every(v => typeof v === 'string');
1076
- const allNumbers = values.every(v => typeof v === 'number');
1077
- if (allStrings) {
1078
- return { isOptional: false, schema: { type: 'string', enum: values.filter((v): v is string => typeof v === 'string') } };
1079
- } else if (allNumbers) {
1080
- const numberValues = values.filter((v): v is number => typeof v === 'number');
1081
- return { isOptional: false, schema: { type: 'number', enum: numberValues } };
1082
- }
1083
- }
1084
- }
1085
- }
1086
- } catch (error) {
1087
- console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
1088
- }
1089
- // If we can't extract enum values, use empty schema (any value)
1090
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1091
- }
1092
-
1093
- // Check if it's a type reference (like InternalFilterType.StringFilter)
1094
- // Type references that aren't objects/arrays/primitives should use 'any'
1095
- if (type.flags & ts.TypeFlags.Object) {
1096
- // Already handled above, but check if it's an empty object type
1097
- const props = checker.getPropertiesOfType(type);
1098
- if (props.length === 0) {
1099
- // Empty object or type reference we can't resolve
1100
- // Check if typeString suggests it's a named type reference
1101
- if (typeString && typeString.includes('.')) {
1102
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1103
- }
1104
- }
1105
- }
1106
-
1107
- // Check if typeString is a valid JSON Schema primitive type
1108
- const lowerTypeString = typeString?.toLowerCase();
1109
-
1110
- // If it's a valid JSON Schema type, use it (shouldn't happen here, but safety check)
1111
- if (lowerTypeString && VALID_JSON_SCHEMA_TYPES.has(lowerTypeString)) {
1112
- // Type assertion is safe here because VALID_JSON_SCHEMA_TYPES validates the value
1113
- const validType = lowerTypeString as 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
1114
- return {
1115
- isOptional: false,
1116
- schema: { type: validType }
1117
- };
1118
- }
1119
-
1120
- // Try to resolve type references before giving up
1121
- const resolvedType = resolveTypeReference(type, checker);
1122
- if (resolvedType) {
1123
- try {
1124
- const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
1125
- if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
1126
- return resolvedResult;
1127
- }
1128
- } catch (error) {
1129
- // Resolution failed, continue to fallback
1130
- }
695
+ // Check cache first
696
+ const cacheKey = `${typeInfo.sourceFile}:${typeInfo.typeName}`;
697
+ if (generatorCache.has(cacheKey)) {
698
+ return generatorCache.get(cacheKey)!;
1131
699
  }
1132
700
 
1133
- // Handle special type flags
1134
- if (type.flags & ts.TypeFlags.Never) {
1135
- return { isOptional: false, schema: { type: 'null' } };
1136
- }
1137
- if (type.flags & ts.TypeFlags.Unknown) {
1138
- return { isOptional: false, schema: {} }; // Empty schema = any value (JSON Schema draft 2020-12)
1139
- }
1140
- if (type.flags & ts.TypeFlags.Any) {
1141
- 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;
1142
706
  }
1143
707
 
1144
- // For any other unrecognized type, use empty schema instead of invalid type strings
1145
- // This prevents errors like "type": "InternalFilterType.StringFilter"
1146
- // Empty schema {} means "allow any value" in JSON Schema draft 2020-12
1147
- return {
1148
- isOptional: false,
1149
- schema: {} // Empty schema = any value (JSON Schema draft 2020-12)
1150
- };
708
+ // Generator failed - return empty schema
709
+ return { isOptional: false, schema: {} }; // Empty schema = any value
1151
710
  }
1152
711
 
1153
712
  /**
@@ -1219,11 +778,13 @@ function extractDefaultValue(
1219
778
  /**
1220
779
  * Extract metadata from a function
1221
780
  */
1222
- function extractFunctionMetadata(
781
+ async function extractFunctionMetadata(
1223
782
  node: ts.FunctionDeclaration | ts.VariableDeclaration,
1224
783
  checker: ts.TypeChecker,
1225
- sourceFile: ts.SourceFile
1226
- ): FunctionMetadata | null {
784
+ sourceFile: ts.SourceFile,
785
+ program?: ts.Program,
786
+ configPath?: string
787
+ ): Promise<FunctionMetadata | null> {
1227
788
  let functionNode: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null = null;
1228
789
  let functionName: string;
1229
790
 
@@ -1286,6 +847,7 @@ function extractFunctionMetadata(
1286
847
 
1287
848
  const properties: Record<string, JsonSchemaProperty> = {};
1288
849
  const required: string[] = [];
850
+ const defs: Record<string, JsonSchemaProperty> = {}; // Type definitions for recursive types (shared across all parameters)
1289
851
 
1290
852
  // Check if async - works for both function declarations and arrow functions
1291
853
  const isAsync =
@@ -1317,6 +879,9 @@ function extractFunctionMetadata(
1317
879
  }
1318
880
  }
1319
881
 
882
+ // Shared defsVisited across all parameters (tracks which definitions we're currently building)
883
+ const defsVisited = new Set<number>();
884
+
1320
885
  for (const param of parameters) {
1321
886
  const paramName = param.name.getText(sourceFile);
1322
887
  const hasDefault = param.initializer !== undefined;
@@ -1340,7 +905,9 @@ function extractFunctionMetadata(
1340
905
  }
1341
906
  }
1342
907
 
1343
- const result = serializeType(type, checker);
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);
1344
911
  propSchema = result.schema;
1345
912
  isOptional = result.isOptional;
1346
913
  } else {
@@ -1398,14 +965,21 @@ function extractFunctionMetadata(
1398
965
  }
1399
966
  }
1400
967
 
968
+ const parametersSchema: FunctionMetadata['parameters'] = {
969
+ type: 'object',
970
+ properties,
971
+ required: required.length > 0 ? required : undefined
972
+ };
973
+
974
+ // Add $defs if we have any type definitions
975
+ if (Object.keys(defs).length > 0) {
976
+ parametersSchema.$defs = defs;
977
+ }
978
+
1401
979
  return {
1402
980
  name: functionName,
1403
981
  description,
1404
- parameters: {
1405
- type: 'object',
1406
- properties,
1407
- required: required.length > 0 ? required : undefined
1408
- },
982
+ parameters: parametersSchema,
1409
983
  returnType,
1410
984
  isAsync,
1411
985
  };
@@ -1474,7 +1048,7 @@ async function autoDiscoverAndExtract(projectRoot: string, outputPath: string) {
1474
1048
  if (sourceFile) {
1475
1049
  const result = findFunctionDefinition(toolName, sourceFile, program);
1476
1050
  if (result) {
1477
- const metadata = extractFunctionMetadata(result.node, checker, result.sourceFile);
1051
+ const metadata = await extractFunctionMetadata(result.node, checker, result.sourceFile, program, configPath);
1478
1052
  if (metadata) {
1479
1053
  functionsMap[toolName] = metadata;
1480
1054
  discoveredFrom.push(path.relative(projectRoot, result.sourceFile.fileName));