@arcteninc/core 0.0.33 → 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], {
|
|
@@ -91,20 +91,34 @@ function extractToolNamesFromExpression(
|
|
|
91
91
|
toolNames.add(element.name.getText(sourceFile));
|
|
92
92
|
}
|
|
93
93
|
// For arrow functions without names, skip them
|
|
94
|
-
} else {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
simpleName.length < 100) {
|
|
106
|
-
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);
|
|
107
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);
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
return;
|
|
@@ -112,7 +126,7 @@ function extractToolNamesFromExpression(
|
|
|
112
126
|
|
|
113
127
|
// Variable reference: toolsList or function name: generateReport
|
|
114
128
|
if (ts.isIdentifier(node)) {
|
|
115
|
-
const varName = node.
|
|
129
|
+
const varName = node.text;
|
|
116
130
|
const varExpr = variableMap.get(varName);
|
|
117
131
|
if (varExpr) {
|
|
118
132
|
extractFromExpr(varExpr);
|
|
@@ -128,7 +142,7 @@ function extractToolNamesFromExpression(
|
|
|
128
142
|
|
|
129
143
|
// Property access: wrappedTools.getRAGInfo
|
|
130
144
|
if (ts.isPropertyAccessExpression(node)) {
|
|
131
|
-
const propName = node.name.
|
|
145
|
+
const propName = node.name.text;
|
|
132
146
|
toolNames.add(propName);
|
|
133
147
|
return;
|
|
134
148
|
}
|
|
@@ -137,7 +151,7 @@ function extractToolNamesFromExpression(
|
|
|
137
151
|
if (ts.isCallExpression(node)) {
|
|
138
152
|
const callExpr = node.expression;
|
|
139
153
|
if (ts.isIdentifier(callExpr)) {
|
|
140
|
-
const funcName = callExpr.
|
|
154
|
+
const funcName = callExpr.text;
|
|
141
155
|
if (funcName === 'useMemo' && node.arguments.length > 0) {
|
|
142
156
|
const firstArg = node.arguments[0];
|
|
143
157
|
// Arrow function: () => [...]
|
|
@@ -370,7 +384,7 @@ function findFunctionDefinition(
|
|
|
370
384
|
|
|
371
385
|
// Follow imports to find definition
|
|
372
386
|
for (const imp of imports) {
|
|
373
|
-
const resolvedPath = resolveImportPath(imp.from, sourceFile.fileName);
|
|
387
|
+
const resolvedPath = resolveImportPath(imp.from, sourceFile.fileName, program);
|
|
374
388
|
if (resolvedPath) {
|
|
375
389
|
const importedSourceFile = program.getSourceFile(resolvedPath);
|
|
376
390
|
if (importedSourceFile) {
|
|
@@ -386,16 +400,81 @@ function findFunctionDefinition(
|
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
/**
|
|
389
|
-
* Resolve import path to actual file path
|
|
403
|
+
* Resolve import path to actual file path using TypeScript's module resolution
|
|
390
404
|
*/
|
|
391
|
-
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
|
|
392
473
|
if (importPath.startsWith('.')) {
|
|
393
|
-
// Relative import
|
|
394
474
|
const dir = path.dirname(fromFile);
|
|
395
475
|
const resolved = path.resolve(dir, importPath);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
476
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.d.ts'];
|
|
477
|
+
|
|
399
478
|
for (const ext of extensions) {
|
|
400
479
|
const withExt = resolved + ext;
|
|
401
480
|
if (fs.existsSync(withExt)) {
|
|
@@ -410,11 +489,197 @@ function resolveImportPath(importPath: string, fromFile: string): string | null
|
|
|
410
489
|
return indexPath;
|
|
411
490
|
}
|
|
412
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
|
+
}
|
|
413
521
|
|
|
414
|
-
|
|
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
|
+
}
|
|
657
|
+
|
|
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
|
+
}
|
|
415
681
|
}
|
|
416
682
|
|
|
417
|
-
// Node modules - we'll skip for now
|
|
418
683
|
return null;
|
|
419
684
|
}
|
|
420
685
|
|
|
@@ -430,11 +695,11 @@ function serializeType(
|
|
|
430
695
|
): { isOptional: boolean; schema: JsonSchemaProperty } {
|
|
431
696
|
const typeString = checker.typeToString(type);
|
|
432
697
|
|
|
433
|
-
if (depth >
|
|
698
|
+
if (depth > MAX_SERIALIZATION_DEPTH) {
|
|
434
699
|
return { isOptional: false, schema: { type: 'any' } };
|
|
435
700
|
}
|
|
436
701
|
|
|
437
|
-
const typeId = (type
|
|
702
|
+
const typeId = getTypeId(type);
|
|
438
703
|
if (typeId !== undefined && visited.has(typeId)) {
|
|
439
704
|
return { isOptional: false, schema: { type: 'any' } };
|
|
440
705
|
}
|
|
@@ -450,23 +715,10 @@ function serializeType(
|
|
|
450
715
|
return { isOptional: false, schema: { type: 'boolean' } };
|
|
451
716
|
}
|
|
452
717
|
|
|
453
|
-
// Handle
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return { isOptional: false, schema: { type: 'boolean', const: value } };
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Handle number literals
|
|
461
|
-
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
462
|
-
const value = (type as any).value;
|
|
463
|
-
return { isOptional: false, schema: { type: 'number', const: value } };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Handle string literals (enum members)
|
|
467
|
-
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
468
|
-
const value = (type as any).value;
|
|
469
|
-
return { isOptional: false, schema: { type: 'string', const: value } };
|
|
718
|
+
// Handle literals using helper function
|
|
719
|
+
const literalSchema = serializeLiteralType(type);
|
|
720
|
+
if (literalSchema) {
|
|
721
|
+
return { isOptional: false, schema: literalSchema };
|
|
470
722
|
}
|
|
471
723
|
|
|
472
724
|
if (type.flags & ts.TypeFlags.Undefined || type.flags & ts.TypeFlags.Void) {
|
|
@@ -486,8 +738,8 @@ function serializeType(
|
|
|
486
738
|
|
|
487
739
|
// Check if it's a string literal union (enum) - possibly with null/undefined
|
|
488
740
|
const stringLiterals = nonNullUndefinedTypes
|
|
489
|
-
.
|
|
490
|
-
.
|
|
741
|
+
.map(t => getStringLiteralValue(t))
|
|
742
|
+
.filter((v): v is string => v !== null);
|
|
491
743
|
|
|
492
744
|
// If all non-null/undefined types are string literals, use enum
|
|
493
745
|
if (stringLiterals.length > 0 && stringLiterals.length === nonNullUndefinedTypes.length) {
|
|
@@ -535,19 +787,12 @@ function serializeType(
|
|
|
535
787
|
anyOf.push({ type: 'number' });
|
|
536
788
|
} else if (unionType.flags & ts.TypeFlags.Boolean) {
|
|
537
789
|
anyOf.push({ type: 'boolean' });
|
|
538
|
-
} else
|
|
539
|
-
//
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const value = (unionType as any).value;
|
|
545
|
-
anyOf.push({ type: 'number', const: value });
|
|
546
|
-
} else if (unionType.flags & ts.TypeFlags.StringLiteral) {
|
|
547
|
-
// String literal (enum member)
|
|
548
|
-
const value = (unionType as any).value;
|
|
549
|
-
anyOf.push({ type: 'string', const: value });
|
|
550
|
-
} else if (checker.isArrayType(unionType)) {
|
|
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)) {
|
|
551
796
|
// Handle array types
|
|
552
797
|
const typeArgs = (unionType as ts.TypeReference).typeArguments;
|
|
553
798
|
if (typeArgs && typeArgs.length > 0) {
|
|
@@ -556,42 +801,40 @@ function serializeType(
|
|
|
556
801
|
} else {
|
|
557
802
|
anyOf.push({ type: 'array' });
|
|
558
803
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const isFallbackAny = refResult.schema.type === 'any' &&
|
|
568
|
-
!refResult.schema.properties &&
|
|
569
|
-
!refResult.schema.anyOf &&
|
|
570
|
-
!refResult.schema.items &&
|
|
571
|
-
typeString.length < 100; // If type string is short, it's probably a simple reference
|
|
572
|
-
|
|
573
|
-
if (!isFallbackAny) {
|
|
574
|
-
anyOf.push(refResult.schema);
|
|
575
|
-
} else {
|
|
576
|
-
// For type references we can't fully resolve (like FilterTreeNode),
|
|
577
|
-
// check if it's a named type that might be defined elsewhere
|
|
578
|
-
// For now, we'll use 'any' but in a more advanced version,
|
|
579
|
-
// we'd track these and generate $defs or inline them
|
|
580
|
-
// Check if it looks like a type reference (starts with capital, no special chars)
|
|
581
|
-
if (/^[A-Z][a-zA-Z0-9]*$/.test(typeString)) {
|
|
582
|
-
// It's a type reference - use any for now
|
|
583
|
-
// TODO: Resolve and inline type definitions or use $ref with $defs
|
|
584
|
-
anyOf.push({ type: 'any' });
|
|
585
|
-
} else {
|
|
586
|
-
// 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)) {
|
|
587
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
|
+
}
|
|
588
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' });
|
|
589
837
|
}
|
|
590
|
-
} catch (e) {
|
|
591
|
-
// Fallback for unresolvable types
|
|
592
|
-
const typeString = checker.typeToString(unionType);
|
|
593
|
-
console.warn(`Warning: Could not serialize union type: ${typeString}`, e);
|
|
594
|
-
anyOf.push({ type: 'any' });
|
|
595
838
|
}
|
|
596
839
|
}
|
|
597
840
|
}
|
|
@@ -624,6 +867,34 @@ function serializeType(
|
|
|
624
867
|
};
|
|
625
868
|
}
|
|
626
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
|
+
|
|
627
898
|
// Handle arrays
|
|
628
899
|
if (checker.isArrayType(type)) {
|
|
629
900
|
const typeArgs = (type as ts.TypeReference).typeArguments;
|
|
@@ -649,9 +920,14 @@ function serializeType(
|
|
|
649
920
|
|
|
650
921
|
if (props.length > 0) {
|
|
651
922
|
for (const prop of props) {
|
|
652
|
-
|
|
923
|
+
try {
|
|
924
|
+
const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
925
|
+
|
|
926
|
+
if (!propDeclaration) {
|
|
927
|
+
// Skip properties without declarations
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
653
930
|
|
|
654
|
-
if (propDeclaration) {
|
|
655
931
|
const propType = checker.getTypeOfSymbolAtLocation(prop, propDeclaration);
|
|
656
932
|
const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
|
|
657
933
|
|
|
@@ -662,6 +938,31 @@ function serializeType(
|
|
|
662
938
|
if (!isOptional && !propResult.isOptional) {
|
|
663
939
|
required.push(prop.name);
|
|
664
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);
|
|
665
966
|
}
|
|
666
967
|
}
|
|
667
968
|
|
|
@@ -683,33 +984,43 @@ function serializeType(
|
|
|
683
984
|
|
|
684
985
|
// Check if it's an enum type (TypeScript enum)
|
|
685
986
|
if (type.flags & ts.TypeFlags.Enum) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
}
|
|
699
1008
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
}
|
|
710
1019
|
}
|
|
711
1020
|
}
|
|
712
1021
|
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
console.warn(`Warning: Could not extract enum values for type: ${typeString}`, error);
|
|
713
1024
|
}
|
|
714
1025
|
// If we can't extract enum values, use 'any'
|
|
715
1026
|
return { isOptional: false, schema: { type: 'any' } };
|
|
@@ -730,17 +1041,42 @@ function serializeType(
|
|
|
730
1041
|
}
|
|
731
1042
|
|
|
732
1043
|
// Check if typeString is a valid JSON Schema primitive type
|
|
733
|
-
const validJsonSchemaTypes = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
|
|
734
1044
|
const lowerTypeString = typeString?.toLowerCase();
|
|
735
1045
|
|
|
736
1046
|
// If it's a valid JSON Schema type, use it (shouldn't happen here, but safety check)
|
|
737
|
-
if (lowerTypeString &&
|
|
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';
|
|
738
1050
|
return {
|
|
739
1051
|
isOptional: false,
|
|
740
|
-
schema: { type:
|
|
1052
|
+
schema: { type: validType }
|
|
741
1053
|
};
|
|
742
1054
|
}
|
|
743
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
|
+
|
|
744
1080
|
// For any other unrecognized type, use 'any' instead of invalid type strings
|
|
745
1081
|
// This prevents errors like "type": "InternalFilterType.StringFilter"
|
|
746
1082
|
return {
|
|
@@ -749,6 +1085,72 @@ function serializeType(
|
|
|
749
1085
|
};
|
|
750
1086
|
}
|
|
751
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
|
+
|
|
752
1154
|
/**
|
|
753
1155
|
* Extract metadata from a function
|
|
754
1156
|
*/
|
|
@@ -836,6 +1238,20 @@ function extractFunctionMetadata(
|
|
|
836
1238
|
? functionNode.parameters
|
|
837
1239
|
: [];
|
|
838
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
|
+
|
|
839
1255
|
for (const param of parameters) {
|
|
840
1256
|
const paramName = param.name.getText(sourceFile);
|
|
841
1257
|
const hasDefault = param.initializer !== undefined;
|
|
@@ -844,7 +1260,21 @@ function extractFunctionMetadata(
|
|
|
844
1260
|
let isOptional = false;
|
|
845
1261
|
|
|
846
1262
|
if (param.type) {
|
|
847
|
-
|
|
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
|
+
|
|
848
1278
|
const result = serializeType(type, checker);
|
|
849
1279
|
propSchema = result.schema;
|
|
850
1280
|
isOptional = result.isOptional;
|
|
@@ -858,9 +1288,23 @@ function extractFunctionMetadata(
|
|
|
858
1288
|
isOptional = true;
|
|
859
1289
|
}
|
|
860
1290
|
|
|
861
|
-
// Add default value if present
|
|
862
|
-
if (hasDefault) {
|
|
863
|
-
|
|
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
|
+
}
|
|
864
1308
|
}
|
|
865
1309
|
|
|
866
1310
|
// Add description from JSDoc
|