@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcteninc/core",
3
- "version": "0.0.32",
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], {
@@ -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
- const name = element.getText(sourceFile).trim();
95
- // Remove any object property access (e.g., tools.getOrders -> getOrders)
96
- const simpleName = name.split('.').pop() || name;
97
- // Skip if it looks like code (contains operators, semicolons, etc.)
98
- if (simpleName &&
99
- simpleName !== 'undefined' &&
100
- simpleName !== 'null' &&
101
- !simpleName.includes('?') &&
102
- !simpleName.includes(';') &&
103
- !simpleName.includes('=>') &&
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.getText(sourceFile);
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.getText(sourceFile);
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.getText(sourceFile);
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(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
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
- // Try common extensions
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
- return resolved;
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 > 10) {
697
+
698
+ if (depth > MAX_SERIALIZATION_DEPTH) {
433
699
  return { isOptional: false, schema: { type: 'any' } };
434
700
  }
435
701
 
436
- const typeId = (type as any).id;
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
- .filter(t => t.flags & ts.TypeFlags.StringLiteral)
469
- .map(t => (t as ts.StringLiteralType).value);
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 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)) {
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
- } else {
527
- // Try to serialize the type (handles objects, type references, etc.)
528
- // This will recursively resolve type aliases and interfaces
529
- try {
530
- const refResult = serializeType(unionType, checker, visited, depth + 1);
531
-
532
- // Check if we got a valid schema (not just 'any' fallback)
533
- const typeString = checker.typeToString(unionType);
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
- 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
+ }
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: typeString.length < 100 ? typeString : 'any' }
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
- 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
+
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
- 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
+ }
767
1308
  }
768
1309
 
769
1310
  // Add description from JSDoc