@arcteninc/core 0.0.32 → 0.0.34
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
|
@@ -35,10 +35,10 @@ async function main() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const args = process.argv.slice(2);
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
// Check for bun first (faster and preferred if available)
|
|
40
40
|
const hasBun = await checkCommand('bun');
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
if (hasBun) {
|
|
43
43
|
// Use bun directly - it's faster and can run TypeScript natively
|
|
44
44
|
const bunProcess = spawn('bun', [tsScript, ...args], {
|
|
@@ -62,7 +62,7 @@ async function main() {
|
|
|
62
62
|
|
|
63
63
|
async function tryTsxFallback(args) {
|
|
64
64
|
const hasNpx = await checkCommand('npx');
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
if (hasNpx) {
|
|
67
67
|
// Use tsx via npx - it's lightweight and works great with Node.js
|
|
68
68
|
const tsxProcess = spawn('npx', ['-y', 'tsx', tsScript, ...args], {
|
|
@@ -14,7 +14,8 @@ interface JsonSchemaProperty {
|
|
|
14
14
|
type?: string | string[]; // Can be string, array of strings, or omitted for anyOf/oneOf/$ref
|
|
15
15
|
description?: string;
|
|
16
16
|
default?: string;
|
|
17
|
-
enum?: string[]; // For enum types
|
|
17
|
+
enum?: (string | number)[]; // For enum types (can be string or number enums)
|
|
18
|
+
const?: string | number | boolean; // For literal values
|
|
18
19
|
items?: JsonSchemaProperty; // For array types
|
|
19
20
|
properties?: Record<string, JsonSchemaProperty>; // For object types
|
|
20
21
|
required?: string[]; // For nested objects
|
|
@@ -90,20 +91,34 @@ function extractToolNamesFromExpression(
|
|
|
90
91
|
toolNames.add(element.name.getText(sourceFile));
|
|
91
92
|
}
|
|
92
93
|
// For arrow functions without names, skip them
|
|
93
|
-
} else {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
simpleName.length < 100) {
|
|
105
|
-
toolNames.add(simpleName);
|
|
94
|
+
} else if (ts.isIdentifier(element)) {
|
|
95
|
+
// Direct identifier: toolName
|
|
96
|
+
const name = element.text;
|
|
97
|
+
if (name && name !== 'undefined' && name !== 'null') {
|
|
98
|
+
toolNames.add(name);
|
|
99
|
+
}
|
|
100
|
+
} else if (ts.isPropertyAccessExpression(element)) {
|
|
101
|
+
// Property access: tools.getOrders -> extract "getOrders"
|
|
102
|
+
const propName = element.name.text;
|
|
103
|
+
if (propName) {
|
|
104
|
+
toolNames.add(propName);
|
|
106
105
|
}
|
|
106
|
+
} else if (ts.isElementAccessExpression(element)) {
|
|
107
|
+
// Element access: tools['getOrders'] -> extract the property name if it's a string literal
|
|
108
|
+
const indexExpr = element.argumentExpression;
|
|
109
|
+
if (ts.isStringLiteral(indexExpr) || ts.isNumericLiteral(indexExpr)) {
|
|
110
|
+
const name = indexExpr.text;
|
|
111
|
+
if (name && name !== 'undefined' && name !== 'null') {
|
|
112
|
+
toolNames.add(name);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// For dynamic access like tools[someVar], recursively extract
|
|
116
|
+
extractFromExpr(indexExpr);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// For other expression types, try to extract recursively
|
|
120
|
+
// This handles cases like: [(someCondition ? tool1 : tool2)]
|
|
121
|
+
extractFromExpr(element);
|
|
107
122
|
}
|
|
108
123
|
}
|
|
109
124
|
return;
|
|
@@ -111,7 +126,7 @@ function extractToolNamesFromExpression(
|
|
|
111
126
|
|
|
112
127
|
// Variable reference: toolsList or function name: generateReport
|
|
113
128
|
if (ts.isIdentifier(node)) {
|
|
114
|
-
const varName = node.
|
|
129
|
+
const varName = node.text;
|
|
115
130
|
const varExpr = variableMap.get(varName);
|
|
116
131
|
if (varExpr) {
|
|
117
132
|
extractFromExpr(varExpr);
|
|
@@ -127,7 +142,7 @@ function extractToolNamesFromExpression(
|
|
|
127
142
|
|
|
128
143
|
// Property access: wrappedTools.getRAGInfo
|
|
129
144
|
if (ts.isPropertyAccessExpression(node)) {
|
|
130
|
-
const propName = node.name.
|
|
145
|
+
const propName = node.name.text;
|
|
131
146
|
toolNames.add(propName);
|
|
132
147
|
return;
|
|
133
148
|
}
|
|
@@ -136,7 +151,7 @@ function extractToolNamesFromExpression(
|
|
|
136
151
|
if (ts.isCallExpression(node)) {
|
|
137
152
|
const callExpr = node.expression;
|
|
138
153
|
if (ts.isIdentifier(callExpr)) {
|
|
139
|
-
const funcName = callExpr.
|
|
154
|
+
const funcName = callExpr.text;
|
|
140
155
|
if (funcName === 'useMemo' && node.arguments.length > 0) {
|
|
141
156
|
const firstArg = node.arguments[0];
|
|
142
157
|
// Arrow function: () => [...]
|
|
@@ -369,7 +384,7 @@ function findFunctionDefinition(
|
|
|
369
384
|
|
|
370
385
|
// Follow imports to find definition
|
|
371
386
|
for (const imp of imports) {
|
|
372
|
-
const resolvedPath = resolveImportPath(imp.from, sourceFile.fileName);
|
|
387
|
+
const resolvedPath = resolveImportPath(imp.from, sourceFile.fileName, program);
|
|
373
388
|
if (resolvedPath) {
|
|
374
389
|
const importedSourceFile = program.getSourceFile(resolvedPath);
|
|
375
390
|
if (importedSourceFile) {
|
|
@@ -385,16 +400,81 @@ function findFunctionDefinition(
|
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
/**
|
|
388
|
-
* Resolve import path to actual file path
|
|
403
|
+
* Resolve import path to actual file path using TypeScript's module resolution
|
|
389
404
|
*/
|
|
390
|
-
function resolveImportPath(
|
|
405
|
+
function resolveImportPath(
|
|
406
|
+
importPath: string,
|
|
407
|
+
fromFile: string,
|
|
408
|
+
program: ts.Program
|
|
409
|
+
): string | null {
|
|
410
|
+
const compilerOptions = program.getCompilerOptions();
|
|
411
|
+
|
|
412
|
+
// Create a minimal module resolution host
|
|
413
|
+
const host: ts.ModuleResolutionHost = {
|
|
414
|
+
fileExists: (fileName: string) => {
|
|
415
|
+
try {
|
|
416
|
+
return fs.existsSync(fileName) && fs.statSync(fileName).isFile();
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
readFile: (fileName: string) => {
|
|
422
|
+
try {
|
|
423
|
+
return fs.readFileSync(fileName, 'utf8');
|
|
424
|
+
} catch {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
directoryExists: (directoryName: string) => {
|
|
429
|
+
try {
|
|
430
|
+
return fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory();
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
getCurrentDirectory: () => path.dirname(fromFile),
|
|
436
|
+
getDirectories: (pathName: string) => {
|
|
437
|
+
try {
|
|
438
|
+
return fs.readdirSync(pathName).filter(name => {
|
|
439
|
+
const fullPath = path.join(pathName, name);
|
|
440
|
+
try {
|
|
441
|
+
return fs.statSync(fullPath).isDirectory();
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
} catch {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
realpath: (pathName: string) => {
|
|
451
|
+
try {
|
|
452
|
+
return fs.realpathSync(pathName);
|
|
453
|
+
} catch {
|
|
454
|
+
return pathName;
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
useCaseSensitiveFileNames: () => process.platform !== 'win32',
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Use TypeScript's module resolution
|
|
461
|
+
const resolved = ts.resolveModuleName(
|
|
462
|
+
importPath,
|
|
463
|
+
fromFile,
|
|
464
|
+
compilerOptions,
|
|
465
|
+
host
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
if (resolved.resolvedModule) {
|
|
469
|
+
return resolved.resolvedModule.resolvedFileName;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fallback: try manual resolution for relative paths
|
|
391
473
|
if (importPath.startsWith('.')) {
|
|
392
|
-
// Relative import
|
|
393
474
|
const dir = path.dirname(fromFile);
|
|
394
475
|
const resolved = path.resolve(dir, importPath);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
476
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.d.ts'];
|
|
477
|
+
|
|
398
478
|
for (const ext of extensions) {
|
|
399
479
|
const withExt = resolved + ext;
|
|
400
480
|
if (fs.existsSync(withExt)) {
|
|
@@ -409,11 +489,197 @@ function resolveImportPath(importPath: string, fromFile: string): string | null
|
|
|
409
489
|
return indexPath;
|
|
410
490
|
}
|
|
411
491
|
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Constants for JSON Schema validation
|
|
498
|
+
// These are actual constants from the JSON Schema specification
|
|
499
|
+
const VALID_JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
|
|
500
|
+
|
|
501
|
+
// Safety limit for serialization depth (only used as a last resort)
|
|
502
|
+
// Note: Cycle detection via 'visited' Set is the primary protection against infinite recursion
|
|
503
|
+
// This is only a fallback safety net for edge cases
|
|
504
|
+
const MAX_SERIALIZATION_DEPTH = 50; // Increased from 10 - legitimate types can be deeply nested
|
|
505
|
+
|
|
506
|
+
// Note: MAX_TOOL_NAME_LENGTH was removed - we now use proper AST node type checks
|
|
507
|
+
// instead of string length heuristics for better accuracy
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Type-safe accessors for TypeScript internal APIs
|
|
511
|
+
* These use type guards to safely access properties that aren't in the public API
|
|
512
|
+
*/
|
|
513
|
+
|
|
514
|
+
interface BooleanLiteralType extends ts.Type {
|
|
515
|
+
intrinsicName: 'true' | 'false';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
interface NumberLiteralType extends ts.Type {
|
|
519
|
+
value: number;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
interface StringLiteralType extends ts.Type {
|
|
523
|
+
value: string;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get boolean literal value from a TypeScript type
|
|
528
|
+
*/
|
|
529
|
+
function getBooleanLiteralValue(type: ts.Type): boolean | null {
|
|
530
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
531
|
+
const boolType = type as BooleanLiteralType;
|
|
532
|
+
return boolType.intrinsicName === 'true';
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Get number literal value from a TypeScript type
|
|
539
|
+
*/
|
|
540
|
+
function getNumberLiteralValue(type: ts.Type): number | null {
|
|
541
|
+
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
542
|
+
const numType = type as NumberLiteralType;
|
|
543
|
+
return numType.value;
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get string literal value from a TypeScript type
|
|
550
|
+
*/
|
|
551
|
+
function getStringLiteralValue(type: ts.Type): string | null {
|
|
552
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
553
|
+
const strType = type as StringLiteralType;
|
|
554
|
+
return strType.value;
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get type ID safely (for cycle detection)
|
|
561
|
+
* Uses TypeScript's internal API - this is necessary for cycle detection
|
|
562
|
+
* but we wrap it in a function to centralize the unsafe access
|
|
563
|
+
*/
|
|
564
|
+
function getTypeId(type: ts.Type): number | undefined {
|
|
565
|
+
// TypeScript's Type interface doesn't expose 'id' in public API,
|
|
566
|
+
// but it's available internally and needed for cycle detection.
|
|
567
|
+
// This is a known limitation when working with TypeScript's compiler API.
|
|
568
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
569
|
+
return (type as any).id;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Serialize a literal type (boolean, number, or string literal) to JSON Schema
|
|
574
|
+
* Returns null if the type is not a literal
|
|
575
|
+
*/
|
|
576
|
+
function serializeLiteralType(type: ts.Type): JsonSchemaProperty | null {
|
|
577
|
+
const boolValue = getBooleanLiteralValue(type);
|
|
578
|
+
if (boolValue !== null) {
|
|
579
|
+
return { type: 'boolean', const: boolValue };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const numValue = getNumberLiteralValue(type);
|
|
583
|
+
if (numValue !== null) {
|
|
584
|
+
return { type: 'number', const: numValue };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const strValue = getStringLiteralValue(type);
|
|
588
|
+
if (strValue !== null) {
|
|
589
|
+
return { type: 'string', const: strValue };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check if a type is a type reference (named type like FilterTreeNode)
|
|
597
|
+
* Uses TypeScript API to determine if it's a named type reference
|
|
598
|
+
*/
|
|
599
|
+
function isTypeReference(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
600
|
+
const symbol = type.getSymbol();
|
|
601
|
+
if (!symbol) return false;
|
|
602
|
+
|
|
603
|
+
const name = symbol.getName();
|
|
604
|
+
if (name.length === 0) return false;
|
|
605
|
+
|
|
606
|
+
// Check if it's a primitive JSON Schema type (should not be treated as type reference)
|
|
607
|
+
if (VALID_JSON_SCHEMA_TYPES.has(name.toLowerCase())) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Check if it's a named type (has declarations and is not a primitive)
|
|
612
|
+
const declarations = symbol.getDeclarations();
|
|
613
|
+
if (!declarations || declarations.length === 0) return false;
|
|
614
|
+
|
|
615
|
+
// Check if any declaration is a type alias, interface, or class
|
|
616
|
+
// This is more reliable than regex pattern matching
|
|
617
|
+
for (const decl of declarations) {
|
|
618
|
+
if (
|
|
619
|
+
ts.isTypeAliasDeclaration(decl) ||
|
|
620
|
+
ts.isInterfaceDeclaration(decl) ||
|
|
621
|
+
ts.isClassDeclaration(decl) ||
|
|
622
|
+
ts.isEnumDeclaration(decl)
|
|
623
|
+
) {
|
|
624
|
+
// Additional check: PascalCase convention (first letter uppercase)
|
|
625
|
+
// This helps distinguish type references from variables/functions
|
|
626
|
+
const firstChar = name.charAt(0);
|
|
627
|
+
return firstChar >= 'A' && firstChar <= 'Z';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Check if a schema is a fallback 'any' (not a real schema)
|
|
636
|
+
* Uses type checking instead of string length heuristics
|
|
637
|
+
*/
|
|
638
|
+
function isFallbackAnySchema(
|
|
639
|
+
schema: JsonSchemaProperty,
|
|
640
|
+
type: ts.Type,
|
|
641
|
+
checker: ts.TypeChecker
|
|
642
|
+
): boolean {
|
|
643
|
+
// If it's not 'any', it's a real schema
|
|
644
|
+
if (schema.type !== 'any') {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// If it has properties, anyOf, or items, it's a real schema
|
|
649
|
+
if (schema.properties || schema.anyOf || schema.items) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Check if it's actually a type reference we couldn't resolve
|
|
654
|
+
// (not a primitive, not an object we processed, etc.)
|
|
655
|
+
return isTypeReference(type, checker);
|
|
656
|
+
}
|
|
412
657
|
|
|
413
|
-
|
|
658
|
+
/**
|
|
659
|
+
* Resolve type references (type aliases, interfaces) to their actual types
|
|
660
|
+
*/
|
|
661
|
+
function resolveTypeReference(
|
|
662
|
+
type: ts.Type,
|
|
663
|
+
checker: ts.TypeChecker
|
|
664
|
+
): ts.Type | null {
|
|
665
|
+
const symbol = type.getSymbol();
|
|
666
|
+
if (!symbol) return null;
|
|
667
|
+
|
|
668
|
+
const declarations = symbol.getDeclarations();
|
|
669
|
+
if (!declarations || declarations.length === 0) return null;
|
|
670
|
+
|
|
671
|
+
// Try to get the actual type from the declaration
|
|
672
|
+
for (const decl of declarations) {
|
|
673
|
+
if (ts.isTypeAliasDeclaration(decl) && decl.type) {
|
|
674
|
+
// Resolve the type alias
|
|
675
|
+
return checker.getTypeFromTypeNode(decl.type);
|
|
676
|
+
}
|
|
677
|
+
if (ts.isInterfaceDeclaration(decl)) {
|
|
678
|
+
// Get the interface type
|
|
679
|
+
return checker.getTypeAtLocation(decl);
|
|
680
|
+
}
|
|
414
681
|
}
|
|
415
682
|
|
|
416
|
-
// Node modules - we'll skip for now
|
|
417
683
|
return null;
|
|
418
684
|
}
|
|
419
685
|
|
|
@@ -428,12 +694,12 @@ function serializeType(
|
|
|
428
694
|
depth = 0
|
|
429
695
|
): { isOptional: boolean; schema: JsonSchemaProperty } {
|
|
430
696
|
const typeString = checker.typeToString(type);
|
|
431
|
-
|
|
432
|
-
if (depth >
|
|
697
|
+
|
|
698
|
+
if (depth > MAX_SERIALIZATION_DEPTH) {
|
|
433
699
|
return { isOptional: false, schema: { type: 'any' } };
|
|
434
700
|
}
|
|
435
701
|
|
|
436
|
-
const typeId = (type
|
|
702
|
+
const typeId = getTypeId(type);
|
|
437
703
|
if (typeId !== undefined && visited.has(typeId)) {
|
|
438
704
|
return { isOptional: false, schema: { type: 'any' } };
|
|
439
705
|
}
|
|
@@ -448,6 +714,13 @@ function serializeType(
|
|
|
448
714
|
if (type.flags & ts.TypeFlags.Boolean) {
|
|
449
715
|
return { isOptional: false, schema: { type: 'boolean' } };
|
|
450
716
|
}
|
|
717
|
+
|
|
718
|
+
// Handle literals using helper function
|
|
719
|
+
const literalSchema = serializeLiteralType(type);
|
|
720
|
+
if (literalSchema) {
|
|
721
|
+
return { isOptional: false, schema: literalSchema };
|
|
722
|
+
}
|
|
723
|
+
|
|
451
724
|
if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
|
|
452
725
|
return { isOptional: true, schema: { type: 'null' } };
|
|
453
726
|
}
|
|
@@ -465,8 +738,8 @@ function serializeType(
|
|
|
465
738
|
|
|
466
739
|
// Check if it's a string literal union (enum) - possibly with null/undefined
|
|
467
740
|
const stringLiterals = nonNullUndefinedTypes
|
|
468
|
-
.
|
|
469
|
-
.
|
|
741
|
+
.map(t => getStringLiteralValue(t))
|
|
742
|
+
.filter((v): v is string => v !== null);
|
|
470
743
|
|
|
471
744
|
// If all non-null/undefined types are string literals, use enum
|
|
472
745
|
if (stringLiterals.length > 0 && stringLiterals.length === nonNullUndefinedTypes.length) {
|
|
@@ -514,7 +787,12 @@ function serializeType(
|
|
|
514
787
|
anyOf.push({ type: 'number' });
|
|
515
788
|
} else if (unionType.flags & ts.TypeFlags.Boolean) {
|
|
516
789
|
anyOf.push({ type: 'boolean' });
|
|
517
|
-
} else
|
|
790
|
+
} else {
|
|
791
|
+
// Try literal types first (using helper function)
|
|
792
|
+
const literalSchema = serializeLiteralType(unionType);
|
|
793
|
+
if (literalSchema) {
|
|
794
|
+
anyOf.push(literalSchema);
|
|
795
|
+
} else if (checker.isArrayType(unionType)) {
|
|
518
796
|
// Handle array types
|
|
519
797
|
const typeArgs = (unionType as ts.TypeReference).typeArguments;
|
|
520
798
|
if (typeArgs && typeArgs.length > 0) {
|
|
@@ -523,42 +801,40 @@ function serializeType(
|
|
|
523
801
|
} else {
|
|
524
802
|
anyOf.push({ type: 'array' });
|
|
525
803
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const isFallbackAny = refResult.schema.type === 'any' &&
|
|
535
|
-
!refResult.schema.properties &&
|
|
536
|
-
!refResult.schema.anyOf &&
|
|
537
|
-
!refResult.schema.items &&
|
|
538
|
-
typeString.length < 100; // If type string is short, it's probably a simple reference
|
|
539
|
-
|
|
540
|
-
if (!isFallbackAny) {
|
|
541
|
-
anyOf.push(refResult.schema);
|
|
542
|
-
} else {
|
|
543
|
-
// For type references we can't fully resolve (like FilterTreeNode),
|
|
544
|
-
// check if it's a named type that might be defined elsewhere
|
|
545
|
-
// For now, we'll use 'any' but in a more advanced version,
|
|
546
|
-
// we'd track these and generate $defs or inline them
|
|
547
|
-
// Check if it looks like a type reference (starts with capital, no special chars)
|
|
548
|
-
if (/^[A-Z][a-zA-Z0-9]*$/.test(typeString)) {
|
|
549
|
-
// It's a type reference - use any for now
|
|
550
|
-
// TODO: Resolve and inline type definitions or use $ref with $defs
|
|
551
|
-
anyOf.push({ type: 'any' });
|
|
552
|
-
} else {
|
|
553
|
-
// It's something else - try to use the serialized result anyway
|
|
804
|
+
} else {
|
|
805
|
+
// Try to serialize the type (handles objects, type references, etc.)
|
|
806
|
+
// This will recursively resolve type aliases and interfaces
|
|
807
|
+
try {
|
|
808
|
+
const refResult = serializeType(unionType, checker, visited, depth + 1);
|
|
809
|
+
|
|
810
|
+
// Check if we got a valid schema (not just 'any' fallback)
|
|
811
|
+
if (!isFallbackAnySchema(refResult.schema, unionType, checker)) {
|
|
554
812
|
anyOf.push(refResult.schema);
|
|
813
|
+
} else {
|
|
814
|
+
// Try to resolve type reference before giving up
|
|
815
|
+
const resolvedType = resolveTypeReference(unionType, checker);
|
|
816
|
+
if (resolvedType) {
|
|
817
|
+
try {
|
|
818
|
+
const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
|
|
819
|
+
if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
|
|
820
|
+
anyOf.push(resolvedResult.schema);
|
|
821
|
+
} else {
|
|
822
|
+
anyOf.push({ type: 'any' });
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
anyOf.push({ type: 'any' });
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
// It's a type reference we couldn't fully resolve - use 'any'
|
|
829
|
+
anyOf.push({ type: 'any' });
|
|
830
|
+
}
|
|
555
831
|
}
|
|
832
|
+
} catch (error) {
|
|
833
|
+
// Fallback for unresolvable types
|
|
834
|
+
const unionTypeString = checker.typeToString(unionType);
|
|
835
|
+
console.warn(`Warning: Could not serialize union type: ${unionTypeString}`, error);
|
|
836
|
+
anyOf.push({ type: 'any' });
|
|
556
837
|
}
|
|
557
|
-
} catch (e) {
|
|
558
|
-
// Fallback for unresolvable types
|
|
559
|
-
const typeString = checker.typeToString(unionType);
|
|
560
|
-
console.warn(`Warning: Could not serialize union type: ${typeString}`, e);
|
|
561
|
-
anyOf.push({ type: 'any' });
|
|
562
838
|
}
|
|
563
839
|
}
|
|
564
840
|
}
|
|
@@ -591,6 +867,34 @@ function serializeType(
|
|
|
591
867
|
};
|
|
592
868
|
}
|
|
593
869
|
|
|
870
|
+
// Handle intersection types (A & B)
|
|
871
|
+
if (type.isIntersection()) {
|
|
872
|
+
const types = type.types;
|
|
873
|
+
const allProperties: Record<string, JsonSchemaProperty> = {};
|
|
874
|
+
const allRequired: string[] = [];
|
|
875
|
+
|
|
876
|
+
for (const intersectionType of types) {
|
|
877
|
+
const result = serializeType(intersectionType, checker, visited, depth + 1);
|
|
878
|
+
if (result.schema.type === 'object' && result.schema.properties) {
|
|
879
|
+
Object.assign(allProperties, result.schema.properties);
|
|
880
|
+
if (result.schema.required) {
|
|
881
|
+
allRequired.push(...result.schema.required);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (Object.keys(allProperties).length > 0) {
|
|
887
|
+
return {
|
|
888
|
+
isOptional: false,
|
|
889
|
+
schema: {
|
|
890
|
+
type: 'object',
|
|
891
|
+
properties: allProperties,
|
|
892
|
+
required: Array.from(new Set(allRequired))
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
594
898
|
// Handle arrays
|
|
595
899
|
if (checker.isArrayType(type)) {
|
|
596
900
|
const typeArgs = (type as ts.TypeReference).typeArguments;
|
|
@@ -616,9 +920,14 @@ function serializeType(
|
|
|
616
920
|
|
|
617
921
|
if (props.length > 0) {
|
|
618
922
|
for (const prop of props) {
|
|
619
|
-
|
|
923
|
+
try {
|
|
924
|
+
const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
925
|
+
|
|
926
|
+
if (!propDeclaration) {
|
|
927
|
+
// Skip properties without declarations
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
620
930
|
|
|
621
|
-
if (propDeclaration) {
|
|
622
931
|
const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
|
|
623
932
|
const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
|
|
624
933
|
|
|
@@ -629,6 +938,31 @@ function serializeType(
|
|
|
629
938
|
if (!isOptional && !propResult.isOptional) {
|
|
630
939
|
required.push(prop.name);
|
|
631
940
|
}
|
|
941
|
+
} catch (error) {
|
|
942
|
+
// Skip properties that can't be serialized
|
|
943
|
+
console.warn(`Warning: Could not serialize property ${prop.name}`, error);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Extract index signatures ([key: string]: T)
|
|
948
|
+
const indexSignatures = checker.getIndexInfosOfType(type);
|
|
949
|
+
if (indexSignatures && indexSignatures.length > 0) {
|
|
950
|
+
try {
|
|
951
|
+
const indexInfo = indexSignatures[0];
|
|
952
|
+
if (indexInfo.declaration) {
|
|
953
|
+
const indexType = checker.getTypeAtLocation(indexInfo.declaration);
|
|
954
|
+
const indexResult = serializeType(indexType, checker, visited, depth + 1);
|
|
955
|
+
// JSON Schema uses additionalProperties for index signatures
|
|
956
|
+
if (indexResult.schema.type !== 'any' || indexResult.schema.properties) {
|
|
957
|
+
// Only set if it's a meaningful type (not just 'any' fallback)
|
|
958
|
+
if (!isFallbackAnySchema(indexResult.schema, indexType, checker)) {
|
|
959
|
+
properties['[key: string]'] = indexResult.schema;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
} catch (error) {
|
|
964
|
+
// Index signature extraction failed, continue without it
|
|
965
|
+
console.warn(`Warning: Could not extract index signature`, error);
|
|
632
966
|
}
|
|
633
967
|
}
|
|
634
968
|
|
|
@@ -645,13 +979,178 @@ function serializeType(
|
|
|
645
979
|
}
|
|
646
980
|
}
|
|
647
981
|
|
|
648
|
-
// Fallback
|
|
982
|
+
// Fallback - check type flags to determine if it's a valid JSON Schema type
|
|
983
|
+
// Valid JSON Schema types: string, number, integer, boolean, object, array, null
|
|
984
|
+
|
|
985
|
+
// Check if it's an enum type (TypeScript enum)
|
|
986
|
+
if (type.flags & ts.TypeFlags.Enum) {
|
|
987
|
+
try {
|
|
988
|
+
const symbol = type.getSymbol();
|
|
989
|
+
if (symbol) {
|
|
990
|
+
const enumMembers = checker.getPropertiesOfType(type);
|
|
991
|
+
if (enumMembers.length > 0) {
|
|
992
|
+
// Extract enum values - enum members are typically string or number literals
|
|
993
|
+
const values: (string | number)[] = [];
|
|
994
|
+
for (const member of enumMembers) {
|
|
995
|
+
const memberDeclaration = member.valueDeclaration || member.declarations?.[0];
|
|
996
|
+
if (memberDeclaration) {
|
|
997
|
+
const memberType = checker.getTypeOfSymbolAtLocation(member, memberDeclaration);
|
|
998
|
+
const strValue = getStringLiteralValue(memberType);
|
|
999
|
+
if (strValue !== null) {
|
|
1000
|
+
values.push(strValue);
|
|
1001
|
+
} else {
|
|
1002
|
+
const numValue = getNumberLiteralValue(memberType);
|
|
1003
|
+
if (numValue !== null) {
|
|
1004
|
+
values.push(numValue);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (values.length > 0) {
|
|
1010
|
+
// Check if all values are strings or all are numbers
|
|
1011
|
+
const allStrings = values.every(v => typeof v === 'string');
|
|
1012
|
+
const allNumbers = values.every(v => typeof v === 'number');
|
|
1013
|
+
if (allStrings) {
|
|
1014
|
+
return { isOptional: false, schema: { type: 'string', enum: values.filter((v): v is string => typeof v === 'string') } };
|
|
1015
|
+
} else if (allNumbers) {
|
|
1016
|
+
const numberValues = values.filter((v): v is number => typeof v === 'number');
|
|
1017
|
+
return { isOptional: false, schema: { type: 'number', enum: numberValues } };
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
|
|
1024
|
+
}
|
|
1025
|
+
// If we can't extract enum values, use 'any'
|
|
1026
|
+
return { isOptional: false, schema: { type: 'any' } };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Check if it's a type reference (like InternalFilterType.StringFilter)
|
|
1030
|
+
// Type references that aren't objects/arrays/primitives should use 'any'
|
|
1031
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
1032
|
+
// Already handled above, but check if it's an empty object type
|
|
1033
|
+
const props = checker.getPropertiesOfType(type);
|
|
1034
|
+
if (props.length === 0) {
|
|
1035
|
+
// Empty object or type reference we can't resolve
|
|
1036
|
+
// Check if typeString suggests it's a named type reference
|
|
1037
|
+
if (typeString && typeString.includes('.')) {
|
|
1038
|
+
return { isOptional: false, schema: { type: 'any' } };
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Check if typeString is a valid JSON Schema primitive type
|
|
1044
|
+
const lowerTypeString = typeString?.toLowerCase();
|
|
1045
|
+
|
|
1046
|
+
// If it's a valid JSON Schema type, use it (shouldn't happen here, but safety check)
|
|
1047
|
+
if (lowerTypeString && VALID_JSON_SCHEMA_TYPES.has(lowerTypeString)) {
|
|
1048
|
+
// Type assertion is safe here because VALID_JSON_SCHEMA_TYPES validates the value
|
|
1049
|
+
const validType = lowerTypeString as 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
|
1050
|
+
return {
|
|
1051
|
+
isOptional: false,
|
|
1052
|
+
schema: { type: validType }
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Try to resolve type references before giving up
|
|
1057
|
+
const resolvedType = resolveTypeReference(type, checker);
|
|
1058
|
+
if (resolvedType) {
|
|
1059
|
+
try {
|
|
1060
|
+
const resolvedResult = serializeType(resolvedType, checker, visited, depth + 1);
|
|
1061
|
+
if (!isFallbackAnySchema(resolvedResult.schema, resolvedType, checker)) {
|
|
1062
|
+
return resolvedResult;
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
// Resolution failed, continue to fallback
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Handle special type flags
|
|
1070
|
+
if (type.flags & ts.TypeFlags.Never) {
|
|
1071
|
+
return { isOptional: false, schema: { type: 'null' } };
|
|
1072
|
+
}
|
|
1073
|
+
if (type.flags & ts.TypeFlags.Unknown) {
|
|
1074
|
+
return { isOptional: false, schema: { type: 'any' } };
|
|
1075
|
+
}
|
|
1076
|
+
if (type.flags & ts.TypeFlags.Any) {
|
|
1077
|
+
return { isOptional: false, schema: { type: 'any' } };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// For any other unrecognized type, use 'any' instead of invalid type strings
|
|
1081
|
+
// This prevents errors like "type": "InternalFilterType.StringFilter"
|
|
649
1082
|
return {
|
|
650
1083
|
isOptional: false,
|
|
651
|
-
schema: { type:
|
|
1084
|
+
schema: { type: 'any' }
|
|
652
1085
|
};
|
|
653
1086
|
}
|
|
654
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* Extract default value from an expression
|
|
1090
|
+
* Evaluates simple literals instead of using getText()
|
|
1091
|
+
*/
|
|
1092
|
+
function extractDefaultValue(
|
|
1093
|
+
initializer: ts.Expression,
|
|
1094
|
+
checker: ts.TypeChecker
|
|
1095
|
+
): any {
|
|
1096
|
+
// Handle string literals
|
|
1097
|
+
if (ts.isStringLiteral(initializer)) {
|
|
1098
|
+
return initializer.text;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Handle numeric literals
|
|
1102
|
+
if (ts.isNumericLiteral(initializer)) {
|
|
1103
|
+
return Number(initializer.text);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Handle boolean literals
|
|
1107
|
+
if (initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
if (initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Handle null
|
|
1115
|
+
if (initializer.kind === ts.SyntaxKind.NullKeyword) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Handle array literals - serialize as JSON string
|
|
1120
|
+
if (ts.isArrayLiteralExpression(initializer)) {
|
|
1121
|
+
try {
|
|
1122
|
+
const elements = initializer.elements
|
|
1123
|
+
.map(el => extractDefaultValue(el, checker))
|
|
1124
|
+
.filter(v => v !== undefined);
|
|
1125
|
+
return JSON.stringify(elements);
|
|
1126
|
+
} catch {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Handle object literals - serialize as JSON string
|
|
1132
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
1133
|
+
try {
|
|
1134
|
+
const obj: Record<string, any> = {};
|
|
1135
|
+
for (const prop of initializer.properties) {
|
|
1136
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
1137
|
+
const key = prop.name.text;
|
|
1138
|
+
const value = extractDefaultValue(prop.initializer, checker);
|
|
1139
|
+
if (value !== undefined) {
|
|
1140
|
+
obj[key] = value;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return JSON.stringify(obj);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return undefined;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// For complex expressions, return undefined (don't include default)
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
655
1154
|
/**
|
|
656
1155
|
* Extract metadata from a function
|
|
657
1156
|
*/
|
|
@@ -739,6 +1238,20 @@ function extractFunctionMetadata(
|
|
|
739
1238
|
? functionNode.parameters
|
|
740
1239
|
: [];
|
|
741
1240
|
|
|
1241
|
+
// Extract generic type parameters and their constraints
|
|
1242
|
+
const genericConstraints = new Map<string, ts.Type>();
|
|
1243
|
+
if (ts.isFunctionDeclaration(node) && node.typeParameters) {
|
|
1244
|
+
for (const typeParam of node.typeParameters) {
|
|
1245
|
+
if (ts.isIdentifier(typeParam.name)) {
|
|
1246
|
+
const paramName = typeParam.name.text;
|
|
1247
|
+
if (typeParam.constraint) {
|
|
1248
|
+
const constraintType = checker.getTypeFromTypeNode(typeParam.constraint);
|
|
1249
|
+
genericConstraints.set(paramName, constraintType);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
742
1255
|
for (const param of parameters) {
|
|
743
1256
|
const paramName = param.name.getText(sourceFile);
|
|
744
1257
|
const hasDefault = param.initializer !== undefined;
|
|
@@ -747,7 +1260,21 @@ function extractFunctionMetadata(
|
|
|
747
1260
|
let isOptional = false;
|
|
748
1261
|
|
|
749
1262
|
if (param.type) {
|
|
750
|
-
|
|
1263
|
+
// Try to get type from type annotation
|
|
1264
|
+
let type = checker.getTypeFromTypeNode(param.type);
|
|
1265
|
+
|
|
1266
|
+
// If it's a generic type parameter, try to use its constraint
|
|
1267
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
1268
|
+
const symbol = type.getSymbol();
|
|
1269
|
+
if (symbol) {
|
|
1270
|
+
const typeParamName = symbol.getName();
|
|
1271
|
+
const constraint = genericConstraints.get(typeParamName);
|
|
1272
|
+
if (constraint) {
|
|
1273
|
+
type = constraint;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
751
1278
|
const result = serializeType(type, checker);
|
|
752
1279
|
propSchema = result.schema;
|
|
753
1280
|
isOptional = result.isOptional;
|
|
@@ -761,9 +1288,23 @@ function extractFunctionMetadata(
|
|
|
761
1288
|
isOptional = true;
|
|
762
1289
|
}
|
|
763
1290
|
|
|
764
|
-
// Add default value if present
|
|
765
|
-
if (hasDefault) {
|
|
766
|
-
|
|
1291
|
+
// Add default value if present - use proper extraction instead of getText()
|
|
1292
|
+
if (hasDefault && param.initializer) {
|
|
1293
|
+
const defaultValue = extractDefaultValue(param.initializer, checker);
|
|
1294
|
+
if (defaultValue !== undefined) {
|
|
1295
|
+
// JSON Schema supports string, number, boolean, and null for default values
|
|
1296
|
+
// The interface allows any via index signature, so we can assign directly
|
|
1297
|
+
if (typeof defaultValue === 'string') {
|
|
1298
|
+
propSchema.default = defaultValue;
|
|
1299
|
+
} else if (typeof defaultValue === 'number' || typeof defaultValue === 'boolean' || defaultValue === null) {
|
|
1300
|
+
// JSON Schema allows these types for default, but TypeScript interface only declares string
|
|
1301
|
+
// Use type assertion to the index signature type which allows any
|
|
1302
|
+
(propSchema as any).default = defaultValue;
|
|
1303
|
+
} else {
|
|
1304
|
+
// For complex values (arrays/objects), store as JSON string
|
|
1305
|
+
propSchema.default = String(defaultValue);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
767
1308
|
}
|
|
768
1309
|
|
|
769
1310
|
// Add description from JSDoc
|