Files
2026-07-13 12:38:34 +08:00

1286 lines
36 KiB
TypeScript

/**
* TypeScript SDK Documentation Generator
*
* Generates MDX documentation from TypeScript source code using TypeDoc.
* Output is written to the docs content directory.
*
* Run: pnpm --filter @composio/core generate:docs
*/
import { readFileSync } from 'fs';
import { mkdir, writeFile, rm, readdir, readFile } from 'fs/promises';
import { join, dirname, resolve } from 'path';
import { execSync } from 'child_process';
import { fileURLToPath, pathToFileURL } from 'url';
// Paths (relative to ts/packages/core)
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const PACKAGE_DIR = join(SCRIPT_DIR, '..');
const MODELS_DIR = join(PACKAGE_DIR, 'src/models');
const OUTPUT_DIR = join(PACKAGE_DIR, '../../../docs/content/reference/sdk-reference/typescript');
const TEMP_JSON = join(PACKAGE_DIR, '.typedoc-output.json');
// Internal classes that should NOT be documented (accessed via other APIs)
const INTERNAL_CLASSES = new Set([
'AuthScheme', // Utility class
'ConnectionRequest', // Utility
'Files', // Not yet stable API
'ToolRouter', // Experimental
]);
// Classes that users instantiate directly (show constructor)
const USER_INSTANTIATED_CLASSES = new Set(['Composio']);
// Pure-docs presentation overrides. The public class names retain their
// legacy "ToolRouter*" names in source (renaming them is a breaking change),
// but the SDK reference presents them under the canonical "Session" naming.
// Keys are the source class names.
const DISPLAY_NAME_OVERRIDES: Record<string, string> = {
ToolRouterSession: 'Session',
ToolRouterSessionFilesMount: 'Session files',
};
const SLUG_OVERRIDES: Record<string, string> = {
ToolRouterSession: 'session',
ToolRouterSessionFilesMount: 'session-files',
};
// Resolve the display title for a class (falls back to the class name).
function displayNameFor(className: string): string {
return DISPLAY_NAME_OVERRIDES[className] ?? className;
}
// Resolve the URL slug / filename stem for a class (falls back to kebab-case).
function slugFor(className: string): string {
return SLUG_OVERRIDES[className] ?? toKebabCase(className);
}
// Discover model files automatically
async function discoverModelFiles(): Promise<string[]> {
const files = await readdir(MODELS_DIR);
return files
.filter(f => f.endsWith('.ts') && !f.includes('.test.') && !f.includes('.spec.'))
.map(f => `src/models/${f}`);
}
// Discover classes to document from TypeDoc output
function discoverClassesToDocument(project: TypeDocProject): string[] {
const classes: string[] = [];
// Always include Composio first
classes.push('Composio');
if (!project.children) return classes;
for (const child of project.children) {
// Direct class
if (child.kind === TYPEDOC_KIND.Class && !INTERNAL_CLASSES.has(child.name)) {
if (!classes.includes(child.name)) {
classes.push(child.name);
}
}
// Class inside a module
if (child.kind === TYPEDOC_KIND.Module && child.children) {
for (const nested of child.children) {
if (nested.kind === TYPEDOC_KIND.Class && !INTERNAL_CLASSES.has(nested.name)) {
if (!classes.includes(nested.name)) {
classes.push(nested.name);
}
}
}
}
}
return classes;
}
// TypeDoc kind numbers (v2.0 schema)
const TYPEDOC_KIND = {
Module: 2,
Class: 128,
Constructor: 512,
Method: 2048,
Property: 1024,
Accessor: 262144,
} as const;
// Helper to find a class by name, searching through modules
function findClass(project: TypeDocProject, className: string): TypeDocReflection | undefined {
if (!project.children) return undefined;
for (const child of project.children) {
// Check if this child is the class we're looking for
if (child.name === className && child.kind === TYPEDOC_KIND.Class) {
return child;
}
// If it's a module, search inside it
if (child.kind === TYPEDOC_KIND.Module && child.children) {
for (const nested of child.children) {
if (nested.name === className && nested.kind === TYPEDOC_KIND.Class) {
return nested;
}
}
}
}
return undefined;
}
interface TypeDocReflection {
id: number;
name: string;
kind: number;
kindString?: string;
comment?: {
summary?: Array<{ kind: string; text: string }>;
blockTags?: Array<{
tag: string;
content: Array<{ kind: string; text: string }>;
}>;
};
signatures?: TypeDocSignature[];
type?: TypeDocType;
children?: TypeDocReflection[];
flags?: {
isPrivate?: boolean;
isProtected?: boolean;
isPublic?: boolean;
isOptional?: boolean;
isStatic?: boolean;
};
sources?: Array<{ fileName: string; line: number }>;
typeParameters?: TypeDocTypeParameter[];
defaultValue?: string;
}
interface TypeDocSignature {
id: number;
name: string;
kind: number;
kindString?: string;
comment?: TypeDocReflection['comment'];
parameters?: TypeDocParameter[];
type?: TypeDocType;
typeParameter?: TypeDocTypeParameter[];
sources?: Array<{ fileName: string; line: number }>;
}
interface TypeDocParameter {
id: number;
name: string;
kind: number;
flags?: { isOptional?: boolean };
type?: TypeDocType;
comment?: TypeDocReflection['comment'];
defaultValue?: string;
}
interface TypeDocType {
type: string;
name?: string;
value?: string | number | boolean;
elementType?: TypeDocType;
types?: TypeDocType[];
typeArguments?: TypeDocType[];
declaration?: TypeDocReflection;
target?: number | TypeDocType;
package?: string;
qualifiedName?: string;
}
interface TypeDocTypeParameter {
id: number;
name: string;
kind: number;
type?: TypeDocType;
default?: TypeDocType;
}
interface TypeDocProject {
id: number;
name: string;
kind: number;
children?: TypeDocReflection[];
}
interface MethodDoc {
name: string;
description: string;
signatures: {
parameters: {
name: string;
type: string;
required: boolean;
description: string;
default?: string;
}[];
returnType: string;
returnDescription?: string;
}[];
examples: string[];
isAsync: boolean;
/** Present when the symbol carries an `@deprecated` JSDoc tag. The string is
* the (possibly empty) tag body, e.g. the recommended replacement. */
deprecated?: string;
source?: { file: string; line: number };
}
interface PropertyDoc {
name: string;
type: string;
description: string;
default?: string;
}
interface ClassDoc {
name: string;
description: string;
constructor?: MethodDoc;
methods: MethodDoc[];
properties: PropertyDoc[];
source?: { file: string; line: number };
/** Present when the class carries a class-level `@deprecated` JSDoc tag. */
deprecated?: string;
}
interface SourceSignatureTypes {
parameters: Map<string, string>;
returnType?: string;
}
const sourceFileCache = new Map<string, string>();
function extractText(content?: Array<{ kind: string; text: string }>): string {
if (!content) return '';
return content
.map(c => c.text)
.join('')
.trim();
}
function extractDescription(comment?: TypeDocReflection['comment']): string {
if (!comment) return '';
let desc = extractText(comment.summary);
// Clean up bare API paths that shouldn't be in user-facing docs, while
// preserving intentional route references inside inline code spans.
desc = desc
.split(/(`[^`]*`)/g)
.map(segment =>
segment.startsWith('`') ? segment : segment.replace(/\/?api\/v\d+\/[^\s`),.;]*/g, '')
)
.join('')
.trim();
// Clean up multiple newlines and spaces
desc = desc
.replace(/\n{3,}/g, '\n\n')
.replace(/ +/g, ' ')
.trim();
return desc;
}
function extractTag(comment: TypeDocReflection['comment'] | undefined, tagName: string): string[] {
if (!comment?.blockTags) return [];
return comment.blockTags.filter(t => t.tag === tagName).map(t => extractText(t.content));
}
function getSourceFileText(fileName: string): string | undefined {
const filePath = join(PACKAGE_DIR, 'src', fileName);
const cached = sourceFileCache.get(filePath);
if (cached !== undefined) return cached;
try {
const text = readFileSync(filePath, 'utf-8');
sourceFileCache.set(filePath, text);
return text;
} catch {
return undefined;
}
}
function getLineOffset(text: string, line: number): number {
let offset = 0;
for (let currentLine = 1; currentLine < line; currentLine++) {
const nextLine = text.indexOf('\n', offset);
if (nextLine === -1) return text.length;
offset = nextLine + 1;
}
return offset;
}
function updateTypeDepth(
char: string,
depth: { angle: number; brace: number; bracket: number; paren: number }
) {
switch (char) {
case '<':
depth.angle++;
break;
case '>':
depth.angle = Math.max(0, depth.angle - 1);
break;
case '{':
depth.brace++;
break;
case '}':
depth.brace = Math.max(0, depth.brace - 1);
break;
case '[':
depth.bracket++;
break;
case ']':
depth.bracket = Math.max(0, depth.bracket - 1);
break;
case '(':
depth.paren++;
break;
case ')':
depth.paren = Math.max(0, depth.paren - 1);
break;
}
}
function splitTopLevel(input: string, delimiter: string): string[] {
const parts: string[] = [];
const depth = { angle: 0, brace: 0, bracket: 0, paren: 0 };
let quote: string | null = null;
let start = 0;
for (let i = 0; i < input.length; i++) {
const char = input[i];
const prev = input[i - 1];
if (quote) {
if (char === quote && prev !== '\\') {
quote = null;
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
updateTypeDepth(char, depth);
if (
char === delimiter &&
depth.angle === 0 &&
depth.brace === 0 &&
depth.bracket === 0 &&
depth.paren === 0
) {
parts.push(input.slice(start, i).trim());
start = i + 1;
}
}
const finalPart = input.slice(start).trim();
if (finalPart) {
parts.push(finalPart);
}
return parts;
}
function findMatchingParen(text: string, openIndex: number): number {
const depth = { angle: 0, brace: 0, bracket: 0, paren: 0 };
let quote: string | null = null;
for (let i = openIndex; i < text.length; i++) {
const char = text[i];
const prev = text[i - 1];
if (quote) {
if (char === quote && prev !== '\\') {
quote = null;
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (char === '(') {
depth.paren++;
} else if (char === ')') {
depth.paren--;
if (depth.paren === 0) return i;
} else {
updateTypeDepth(char, depth);
}
}
return -1;
}
function findTopLevelChar(input: string, target: string): number {
const depth = { angle: 0, brace: 0, bracket: 0, paren: 0 };
let quote: string | null = null;
for (let i = 0; i < input.length; i++) {
const char = input[i];
const prev = input[i - 1];
if (quote) {
if (char === quote && prev !== '\\') {
quote = null;
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
if (
char === target &&
!(target === '=' && input[i + 1] === '>') &&
depth.angle === 0 &&
depth.brace === 0 &&
depth.bracket === 0 &&
depth.paren === 0
) {
return i;
}
updateTypeDepth(char, depth);
}
return -1;
}
function findReturnTypeColon(text: string, start: number): number {
const depth = { angle: 0, brace: 0, bracket: 0, paren: 0 };
let quote: string | null = null;
for (let i = start; i < text.length; i++) {
const char = text[i];
const prev = text[i - 1];
if (quote) {
if (char === quote && prev !== '\\') {
quote = null;
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
const isTopLevel =
depth.angle === 0 && depth.brace === 0 && depth.bracket === 0 && depth.paren === 0;
if (isTopLevel) {
if (char === ':') return i;
if (char === '{' || char === ';') return -1;
}
updateTypeDepth(char, depth);
}
return -1;
}
function findSignatureEnd(text: string, start: number): number {
const depth = { angle: 0, brace: 0, bracket: 0, paren: 0 };
let quote: string | null = null;
for (let i = start; i < text.length; i++) {
const char = text[i];
const prev = text[i - 1];
if (quote) {
if (char === quote && prev !== '\\') {
quote = null;
}
continue;
}
if (char === "'" || char === '"' || char === '`') {
quote = char;
continue;
}
const isTopLevel =
depth.angle === 0 && depth.brace === 0 && depth.bracket === 0 && depth.paren === 0;
const startsInlineObjectType =
char === '{' &&
[':', '|', '&', '(', ',', '='].includes(previousNonWhitespace(text, i - 1) ?? '');
if ((char === ';' || (char === '{' && !startsInlineObjectType)) && isTopLevel) {
return i;
}
updateTypeDepth(char, depth);
}
return -1;
}
function previousNonWhitespace(text: string, index: number): string | undefined {
for (let i = index; i >= 0; i--) {
const char = text[i];
if (!/\s/.test(char)) {
return char;
}
}
return undefined;
}
function normalizeSourceParamName(name: string): string | undefined {
const normalized = name
.trim()
.replace(/^\.\.\./, '')
.split(/\s+/)
.pop()
?.replace(/\?$/, '');
if (!normalized || normalized.startsWith('{') || normalized.startsWith('[')) {
return undefined;
}
return normalized;
}
function parseSourceParameter(param: string): [string, string] | undefined {
const colonIndex = findTopLevelChar(param, ':');
if (colonIndex === -1) return undefined;
const name = normalizeSourceParamName(param.slice(0, colonIndex));
if (!name) return undefined;
let type = param.slice(colonIndex + 1).trim();
const defaultIndex = findTopLevelChar(type, '=');
if (defaultIndex !== -1) {
type = type.slice(0, defaultIndex).trim();
}
return [name, type];
}
export function parseSourceSignatureTypesAtLine(
text: string,
line: number
): SourceSignatureTypes | undefined {
const lineOffset = getLineOffset(text, line);
const openParen = text.indexOf('(', lineOffset);
if (openParen === -1) return undefined;
const closeParen = findMatchingParen(text, openParen);
if (closeParen === -1) return undefined;
const parameters = new Map<string, string>();
const paramsText = text.slice(openParen + 1, closeParen).trim();
for (const param of splitTopLevel(paramsText, ',')) {
const parsed = parseSourceParameter(param);
if (parsed) {
parameters.set(parsed[0], parsed[1]);
}
}
const colonIndex = findReturnTypeColon(text, closeParen + 1);
if (colonIndex === -1) return { parameters };
const absoluteReturnStart = colonIndex + 1;
const returnEnd = findSignatureEnd(text, absoluteReturnStart);
const returnType =
returnEnd === -1
? text.slice(absoluteReturnStart).trim()
: text.slice(absoluteReturnStart, returnEnd).trim();
return { parameters, returnType };
}
function getSourceSignatureTypes(signature: TypeDocSignature): SourceSignatureTypes | undefined {
const source = signature.sources?.[0];
if (!source) return undefined;
const text = getSourceFileText(source.fileName);
if (!text) return undefined;
return parseSourceSignatureTypesAtLine(text, source.line);
}
function formatYamlFrontmatterString(value: string): string {
return JSON.stringify(value);
}
function formatType(type?: TypeDocType, depth = 0): string {
if (!type) return 'unknown';
if (depth > 5) return '...'; // Prevent infinite recursion
switch (type.type) {
case 'intrinsic':
return type.name || 'unknown';
case 'literal':
if (typeof type.value === 'string') return `'${type.value}'`;
return String(type.value);
case 'reference':
if (type.typeArguments && type.typeArguments.length > 0) {
const args = type.typeArguments.map(t => formatType(t, depth + 1)).join(', ');
return `${type.name}<${args}>`;
}
return type.name || 'unknown';
case 'array':
return `${formatType(type.elementType, depth + 1)}[]`;
case 'union':
if (type.types) {
return type.types.map(t => formatType(t, depth + 1)).join(' | ');
}
return 'unknown';
case 'intersection':
if (type.types) {
return type.types.map(t => formatType(t, depth + 1)).join(' & ');
}
return 'unknown';
case 'reflection':
if (type.declaration?.signatures) {
// Function type
const sig = type.declaration.signatures[0];
const params =
sig.parameters?.map(p => `${p.name}: ${formatType(p.type, depth + 1)}`).join(', ') || '';
return `(${params}) => ${formatType(sig.type, depth + 1)}`;
}
if (type.declaration?.children) {
// Object type
const props = type.declaration.children
.map(c => {
const opt = c.flags?.isOptional ? '?' : '';
return `${c.name}${opt}: ${formatType(c.type, depth + 1)}`;
})
.join('; ');
return `{ ${props} }`;
}
return 'object';
case 'tuple':
if (type.types) {
return `[${type.types.map(t => formatType(t, depth + 1)).join(', ')}]`;
}
return '[]';
case 'query':
return `typeof ${formatType(type.target as TypeDocType, depth + 1)}`;
case 'typeOperator':
return type.name || 'unknown';
default:
return type.name || 'unknown';
}
}
function extractMethod(reflection: TypeDocReflection): MethodDoc | null {
if (!reflection.signatures || reflection.signatures.length === 0) {
return null;
}
const signatures = reflection.signatures.map(sig => {
const sourceTypes = getSourceSignatureTypes(sig);
const parameters = (sig.parameters || []).map(param => ({
// Clean up ugly TypeScript internal names
name: param.name.startsWith('__') ? 'options' : param.name,
type: sourceTypes?.parameters.get(param.name) ?? formatType(param.type),
required: !param.flags?.isOptional,
description: extractDescription(param.comment),
default: param.defaultValue,
}));
return {
parameters,
returnType: sourceTypes?.returnType ?? formatType(sig.type),
returnDescription: extractTag(sig.comment, '@returns')[0],
};
});
// Use the first signature's comment for the method description
const primarySig = reflection.signatures[0];
const description = extractDescription(primarySig.comment);
const examples = extractTag(primarySig.comment, '@example');
// `@deprecated` may live on the signature comment or the reflection comment.
const deprecated =
extractTag(primarySig.comment, '@deprecated')[0] ??
extractTag(reflection.comment, '@deprecated')[0];
return {
name: reflection.name,
description,
signatures,
examples,
isAsync:
signatures[0]?.returnType.startsWith('Promise') ??
formatType(primarySig.type).startsWith('Promise'),
deprecated,
source: reflection.sources?.[0]
? { file: reflection.sources[0].fileName, line: reflection.sources[0].line }
: undefined,
};
}
function extractClass(reflection: TypeDocReflection): ClassDoc {
const classDoc: ClassDoc = {
name: reflection.name,
description: extractDescription(reflection.comment),
methods: [],
properties: [],
source: reflection.sources?.[0]
? { file: reflection.sources[0].fileName, line: reflection.sources[0].line }
: undefined,
deprecated: extractTag(reflection.comment, '@deprecated')[0],
};
if (!reflection.children) return classDoc;
for (const child of reflection.children) {
// Skip private/protected members
if (child.flags?.isPrivate || child.flags?.isProtected) continue;
// Constructor (kind 512)
if (child.kind === TYPEDOC_KIND.Constructor) {
const method = extractMethod(child);
if (method) {
classDoc.constructor = method;
}
continue;
}
// Methods (kind 2048)
if (child.kind === TYPEDOC_KIND.Method) {
const method = extractMethod(child);
if (method) {
classDoc.methods.push(method);
}
continue;
}
// Properties (kind 1024, public only)
if (child.kind === TYPEDOC_KIND.Property && !child.flags?.isPrivate) {
classDoc.properties.push({
name: child.name,
type: formatType(child.type),
description: extractDescription(child.comment),
default: child.defaultValue,
});
}
}
return classDoc;
}
function generateMethodMdx(method: MethodDoc): string {
const lines: string[] = [];
// Method header. Flag deprecated methods in the heading so they read clearly
// in the sidebar/anchor and at a glance.
const deprecatedSuffix = method.deprecated !== undefined ? ' (deprecated)' : '';
lines.push(`### ${method.name}()${deprecatedSuffix}`);
lines.push('');
// Deprecation callout (rendered as a fumadocs warning callout).
if (method.deprecated !== undefined) {
const note = method.deprecated.trim();
lines.push('<Callout type="warn" title="Deprecated">');
lines.push(note.length > 0 ? escapeTextForMdx(note) : 'This method is deprecated.');
lines.push('</Callout>');
lines.push('');
}
// Description
if (method.description) {
lines.push(method.description);
lines.push('');
}
// Handle multiple overloads
for (let i = 0; i < method.signatures.length; i++) {
const sig = method.signatures[i];
if (method.signatures.length > 1) {
lines.push(`**Overload ${i + 1}**`);
lines.push('');
}
// Signature
const params = sig.parameters
.map(p => {
const opt = p.required ? '' : '?';
const paramType = simplifyTypeForSignature(p.type);
return `${p.name}${opt}: ${paramType}`;
})
.join(', ');
const asyncPrefix = method.isAsync ? 'async ' : '';
const returnType = simplifyTypeForSignature(sig.returnType);
lines.push('```typescript');
lines.push(`${asyncPrefix}${method.name}(${params}): ${returnType}`);
lines.push('```');
lines.push('');
// Parameters table
if (sig.parameters.length > 0) {
lines.push('**Parameters**');
lines.push('');
// Check if any parameter has a description
const hasDescriptions = sig.parameters.some(p => p.description);
if (hasDescriptions) {
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const param of sig.parameters) {
const opt = param.required ? '' : '?';
const desc = escapeTextForMdx(param.description || '');
const typeCell = escapeTypeForMdx(simplifyTypeForTable(param.type));
lines.push(`| \`${param.name}${opt}\` | \`${typeCell}\` | ${desc} |`);
}
} else {
// No descriptions - use simpler table
lines.push('| Name | Type |');
lines.push('|------|------|');
for (const param of sig.parameters) {
const opt = param.required ? '' : '?';
const typeCell = escapeTypeForMdx(simplifyTypeForTable(param.type));
lines.push(`| \`${param.name}${opt}\` | \`${typeCell}\` |`);
}
}
lines.push('');
}
// Return type
if (sig.returnType !== 'void') {
lines.push('**Returns**');
lines.push('');
const returnTypeDisplay = escapeTypeForMdx(simplifyTypeForTable(sig.returnType));
let returnLine = `\`${returnTypeDisplay}\``;
if (sig.returnDescription) {
returnLine += ` — ${escapeTextForMdx(sig.returnDescription)}`;
}
lines.push(returnLine);
lines.push('');
}
}
// Examples
if (method.examples.length > 0) {
lines.push('**Example**');
lines.push('');
for (const example of method.examples) {
// Clean up the example - remove surrounding ```typescript blocks if present
let cleanExample = example.trim();
if (cleanExample.startsWith('```typescript')) {
cleanExample = cleanExample.slice('```typescript'.length);
}
if (cleanExample.startsWith('```ts')) {
cleanExample = cleanExample.slice('```ts'.length);
}
if (cleanExample.endsWith('```')) {
cleanExample = cleanExample.slice(0, -3);
}
cleanExample = cleanExample.trim();
lines.push('```typescript');
lines.push(cleanExample);
lines.push('```');
lines.push('');
}
}
lines.push('---');
lines.push('');
return lines.join('\n');
}
function generateClassMdx(classDoc: ClassDoc): string {
const lines: string[] = [];
// Build full description for frontmatter (first sentence + additional context)
const fullDescription = classDoc.description
? classDoc.description.replace(/\n/g, ' ').trim()
: `${classDoc.name} class reference`;
// Frontmatter - fumadocs renders title and description automatically
lines.push('---');
lines.push(`title: ${formatYamlFrontmatterString(displayNameFor(classDoc.name))}`);
lines.push(`description: ${formatYamlFrontmatterString(fullDescription)}`);
lines.push('---');
lines.push('');
// Class-level deprecation callout (rendered as a fumadocs warning callout).
if (classDoc.deprecated !== undefined) {
const note = classDoc.deprecated.trim();
lines.push('<Callout type="warn" title="Deprecated">');
lines.push(note.length > 0 ? escapeTextForMdx(note) : 'This class is deprecated.');
lines.push('</Callout>');
lines.push('');
}
// Content starts directly with Constructor or Usage (no duplicate title/description)
// Constructor - only show for user-instantiated classes
if (classDoc.constructor && USER_INSTANTIATED_CLASSES.has(classDoc.name)) {
lines.push('## Constructor');
lines.push('');
lines.push(generateMethodMdx(classDoc.constructor));
} else if (!USER_INSTANTIATED_CLASSES.has(classDoc.name) && !(classDoc.name in SLUG_OVERRIDES)) {
// Skip the `composio.<accessor>` Usage block for session-object classes —
// they are not accessed as a `composio` sub-client property; the class
// description explains how to obtain them (via `composio.sessions`).
lines.push('## Usage');
lines.push('');
// Handle acronyms (e.g., "MCP" -> "mcp") vs PascalCase (e.g., "AuthConfigs" -> "authConfigs")
const accessorName =
classDoc.name === classDoc.name.toUpperCase()
? classDoc.name.toLowerCase()
: classDoc.name.charAt(0).toLowerCase() + classDoc.name.slice(1);
lines.push(`Access this class through the \`composio.${accessorName}\` property:`);
lines.push('');
lines.push('```typescript');
lines.push(`const composio = new Composio({ apiKey: 'your-api-key' });`);
lines.push(`const result = await composio.${accessorName}.list();`);
lines.push('```');
lines.push('');
}
// Properties (if any public ones exist)
const publicProps = classDoc.properties.filter(p => !p.name.startsWith('_'));
if (publicProps.length > 0) {
lines.push('## Properties');
lines.push('');
// Check if any property has a description
const hasDescriptions = publicProps.some(p => p.description);
if (hasDescriptions) {
lines.push('| Name | Type | Description |');
lines.push('|------|------|-------------|');
for (const prop of publicProps) {
const typeCell = escapeTypeForMdx(simplifyTypeForTable(prop.type));
const safeDesc = escapeTextForMdx(prop.description || '');
lines.push(`| \`${prop.name}\` | \`${typeCell}\` | ${safeDesc} |`);
}
} else {
lines.push('| Name | Type |');
lines.push('|------|------|');
for (const prop of publicProps) {
const typeCell = escapeTypeForMdx(simplifyTypeForTable(prop.type));
lines.push(`| \`${prop.name}\` | \`${typeCell}\` |`);
}
}
lines.push('');
}
// Methods
if (classDoc.methods.length > 0) {
lines.push('## Methods');
lines.push('');
for (const method of classDoc.methods) {
lines.push(generateMethodMdx(method));
}
}
return lines.join('\n');
}
function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase();
}
// Escape text content for MDX (descriptions, etc. that appear outside backticks)
// Handles both curly braces and angle brackets (which MDX interprets as JSX tags)
function escapeTextForMdx(str: string): string {
return str
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Clean up internal generic type parameters that don't add value for users
function cleanupGenericTypes(type: string): string {
// Remove generic parameters that are just implementation details
// e.g., "Tools<unknown, unknown, TProvider>" -> "Tools"
// e.g., "Composio<TProvider>" -> "Composio"
// e.g., "Uint8Array<ArrayBufferLike>" -> "Uint8Array"
let cleaned = type;
// Remove type parameters that are just unknowns or TProvider
cleaned = cleaned.replace(/<unknown(?:,\s*unknown)*(?:,\s*TProvider)?>/g, '');
cleaned = cleaned.replace(/<TProvider>/g, '');
cleaned = cleaned.replace(/<unknown>/g, '');
// Remove TypeScript internal generic parameters (e.g., <ArrayBufferLike>)
// that add no value for users and break MDX parsing
cleaned = cleaned.replace(/<ArrayBufferLike>/g, '');
// Clean up double spaces and trailing commas
cleaned = cleaned.replace(/\s+/g, ' ').trim();
return cleaned;
}
// Escape type strings for safe use in MDX backtick code spans.
// Escapes backslashes first so later Markdown escapes cannot be neutralized.
export function escapeTypeForMdx(type: string): string {
return type
.replace(/\\/g, '\\\\')
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}')
.replace(/\|/g, '\\|');
}
function isInlineObjectType(type: string): boolean {
return type.startsWith('{') || type.includes(': {');
}
// Simplify complex types for table display while preserving public object shapes.
export function simplifyTypeForTable(type: string): string {
// First clean up internal generics
const cleaned = cleanupGenericTypes(type);
if (isInlineObjectType(cleaned)) {
return cleaned;
}
// If type is too long or complex, simplify it
if (cleaned.length > 80) {
// Extract just the outer type name if it's a generic
const genericMatch = cleaned.match(/^([A-Za-z]+)<.*>$/);
if (genericMatch) {
return `${genericMatch[1]}<...>`;
}
// For function types, simplify
if (cleaned.includes('=>')) {
const returnMatch = cleaned.match(/=>\s*(.+)$/);
const returnType = returnMatch ? returnMatch[1] : 'unknown';
return `(...) => ${simplifyTypeForTable(returnType)}`;
}
// Truncate very long types
if (cleaned.length > 80) {
return cleaned.substring(0, 77) + '...';
}
}
return cleaned;
}
// Simplify types for code block display (less aggressive, preserves structure)
export function simplifyTypeForSignature(type: string): string {
// First clean up internal generics
const cleaned = cleanupGenericTypes(type);
if (isInlineObjectType(cleaned)) {
return cleaned;
}
// Keep types under 100 chars as-is
if (cleaned.length <= 100) {
return cleaned;
}
// For Promise<...> with complex inner type, simplify inner
const promiseMatch = cleaned.match(/^Promise<(.+)>$/);
if (promiseMatch) {
const inner = promiseMatch[1];
if (inner.length > 60 || inner.includes(': {')) {
return 'Promise<...>';
}
}
// For generics with complex type arguments
const genericMatch = cleaned.match(/^([A-Za-z]+)<(.+)>$/);
if (genericMatch) {
const [, name, args] = genericMatch;
if (args.length > 60 || args.includes(': {')) {
return `${name}<...>`;
}
}
// Truncate very long types
if (cleaned.length > 100) {
return cleaned.substring(0, 97) + '...';
}
return cleaned;
}
async function runTypeDoc(): Promise<TypeDocProject> {
console.log('Running TypeDoc...');
// Auto-discover entry points from models directory
const modelFiles = await discoverModelFiles();
const entryPoints = ['src/composio.ts', ...modelFiles];
console.log(` Found ${entryPoints.length} entry points`);
const cmd = [
'npx typedoc',
'--json',
TEMP_JSON,
'--tsconfig',
'tsconfig.json',
'--excludePrivate',
'--excludeProtected',
'--excludeInternal',
'--skipErrorChecking', // Skip TS errors, we just want the documentation
...entryPoints,
].join(' ');
try {
execSync(cmd, { stdio: 'pipe', cwd: PACKAGE_DIR });
} catch (error) {
console.error('TypeDoc failed:', error);
throw error;
}
const jsonContent = await readFile(TEMP_JSON, 'utf-8');
return JSON.parse(jsonContent) as TypeDocProject;
}
async function main() {
console.log('Starting TypeScript SDK documentation generation...\n');
console.log(`Package dir: ${PACKAGE_DIR}`);
console.log(`Output dir: ${OUTPUT_DIR}\n`);
// Clean output directory
try {
await rm(OUTPUT_DIR, { recursive: true, force: true });
} catch {
// Directory doesn't exist, that's fine
}
await mkdir(OUTPUT_DIR, { recursive: true });
// Run TypeDoc
const project = await runTypeDoc();
if (!project.children) {
throw new Error('No classes found in TypeDoc output');
}
// Auto-discover classes to document (excludes internal classes)
const classesToDocument = discoverClassesToDocument(project);
console.log(
` Found ${classesToDocument.length} classes to document: ${classesToDocument.join(', ')}`
);
// Find and document each class
const documented: { name: string; description: string }[] = [];
for (const className of classesToDocument) {
const reflection = findClass(project, className);
if (!reflection) {
console.warn(` Warning: ${className} not found in TypeDoc output`);
continue;
}
console.log(` Processing ${className}...`);
const classDoc = extractClass(reflection);
const mdx = generateClassMdx(classDoc);
const fileName = slugFor(className) + '.mdx';
const filePath = join(OUTPUT_DIR, fileName);
await writeFile(filePath, mdx);
documented.push({
name: className,
description: classDoc.description.split('\n')[0] || `${className} API`,
});
}
// Generate index page
const classesTable = documented
.map(
({ name, description }) =>
`| [\`${displayNameFor(name)}\`](/reference/sdk-reference/typescript/${slugFor(name)}) | ${escapeTextForMdx(description)} |`
)
.join('\n');
const indexContent = `---
title: ${formatYamlFrontmatterString('TypeScript SDK Reference')}
description: ${formatYamlFrontmatterString('Complete API reference for the Composio TypeScript SDK (@composio/core).')}
---
## Installation
<PackageInstall packages="@composio/core" />
## Classes
| Class | Description |
|-------|-------------|
${classesTable}
## Quick Start
\`\`\`typescript
import { Composio } from '@composio/core';
const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY
});
// Get tools for a user
const tools = await composio.tools.get('user-123', {
toolkits: ['github']
});
// Execute a tool
const result = await composio.tools.execute('GITHUB_GET_REPOS', {
userId: 'user-123',
arguments: { owner: 'composio' }
});
\`\`\`
`;
await writeFile(join(OUTPUT_DIR, 'index.mdx'), indexContent);
// Generate meta.json for sidebar
const meta = {
title: 'TypeScript SDK',
pages: documented.map(({ name }) => slugFor(name)),
};
await writeFile(join(OUTPUT_DIR, 'meta.json'), JSON.stringify(meta, null, 2));
// Clean up temp file
try {
await rm(TEMP_JSON);
} catch {
// Ignore cleanup errors
}
console.log('\nGeneration complete!');
console.log(` Output: ${OUTPUT_DIR}`);
console.log(` Classes: ${documented.map(d => d.name).join(', ')}`);
console.log(` Files generated: ${documented.length + 2}`); // +2 for index.mdx and meta.json
}
function isDirectRun(): boolean {
const entrypoint = process.argv[1];
return Boolean(entrypoint && import.meta.url === pathToFileURL(resolve(entrypoint)).href);
}
if (isDirectRun()) {
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
}