@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 +2 -1
- package/scripts/cli-extract-types-auto.ts +138 -564
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcteninc/core",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
*
|
|
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
|
|
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
|
-
):
|
|
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
|
-
//
|
|
591
|
+
// Find the first declaration that's a type alias, interface, or class
|
|
736
592
|
for (const decl of declarations) {
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
616
|
+
async function serializeTypeWithGenerator(
|
|
755
617
|
type: ts.Type,
|
|
756
618
|
checker: ts.TypeChecker,
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
//
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
833
|
-
const
|
|
645
|
+
const generator = tsJsonSchemaGeneratorModule.createGenerator(generatorConfig);
|
|
646
|
+
const schema = generator.createSchema(typeInfo.typeName);
|
|
834
647
|
|
|
835
|
-
//
|
|
836
|
-
if (
|
|
837
|
-
|
|
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
|
-
//
|
|
841
|
-
|
|
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
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
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:
|
|
930
|
-
schema:
|
|
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
|
-
|
|
935
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
976
|
-
if (
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
//
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
-
//
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
//
|
|
1145
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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));
|