@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcteninc/core",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -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
- const name = element.getText(sourceFile).trim();
96
- // Remove any object property access (e.g., tools.getOrders -> getOrders)
97
- const simpleName = name.split('.').pop() || name;
98
- // Skip if it looks like code (contains operators, semicolons, etc.)
99
- if (simpleName &&
100
- simpleName !== 'undefined' &&
101
- simpleName !== 'null' &&
102
- !simpleName.includes('?') &&
103
- !simpleName.includes(';') &&
104
- !simpleName.includes('=>') &&
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.getText(sourceFile);
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.getText(sourceFile);
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.getText(sourceFile);
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(importPath: string, fromFile: string): string | null {
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
- // Try common extensions
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
- return resolved;
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 > 10) {
698
+ if (depth > MAX_SERIALIZATION_DEPTH) {
434
699
  return { isOptional: false, schema: { type: 'any' } };
435
700
  }
436
701
 
437
- const typeId = (type as any).id;
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 boolean literals (true/false)
454
- if (type.flags & ts.TypeFlags.BooleanLiteral) {
455
- // Access the value through the intrinsicName property
456
- const value = (type as any).intrinsicName === 'true';
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
- .filter(t => t.flags & ts.TypeFlags.StringLiteral)
490
- .map(t => (t as ts.StringLiteralType).value);
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 if (unionType.flags & ts.TypeFlags.BooleanLiteral) {
539
- // Boolean literal (true/false)
540
- const value = (unionType as any).intrinsicName === 'true';
541
- anyOf.push({ type: 'boolean', const: value });
542
- } else if (unionType.flags & ts.TypeFlags.NumberLiteral) {
543
- // Number literal
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
- } else {
560
- // Try to serialize the type (handles objects, type references, etc.)
561
- // This will recursively resolve type aliases and interfaces
562
- try {
563
- const refResult = serializeType(unionType, checker, visited, depth + 1);
564
-
565
- // Check if we got a valid schema (not just 'any' fallback)
566
- const typeString = checker.typeToString(unionType);
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
- const propDeclaration = prop.valueDeclaration || prop.declarations?.[0];
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
- // Try to get enum members
687
- const symbol = type.symbol || (type as any).symbol;
688
- if (symbol) {
689
- const enumMembers = checker.getPropertiesOfType(type);
690
- if (enumMembers.length > 0) {
691
- // Extract enum values - enum members are typically string or number literals
692
- const values: (string | number)[] = [];
693
- for (const member of enumMembers) {
694
- const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration || member.declarations?.[0] || type as any);
695
- if (memberType.flags & ts.TypeFlags.StringLiteral) {
696
- values.push((memberType as any).value);
697
- } else if (memberType.flags & ts.TypeFlags.NumberLiteral) {
698
- values.push((memberType as any).value);
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
- if (values.length > 0) {
702
- // Check if all values are strings or all are numbers
703
- const allStrings = values.every(v => typeof v === 'string');
704
- const allNumbers = values.every(v => typeof v === 'number');
705
- if (allStrings) {
706
- return { isOptional: false, schema: { type: 'string', enum: values.filter((v): v is string => typeof v === 'string') } };
707
- } else if (allNumbers) {
708
- const numberValues = values.filter((v): v is number => typeof v === 'number');
709
- return { isOptional: false, schema: { type: 'number', enum: numberValues } };
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 && validJsonSchemaTypes.has(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: lowerTypeString as any }
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
- const type = checker.getTypeFromTypeNode(param.type);
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
- propSchema.default = param.initializer!.getText(sourceFile);
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