Writer
Functions
writeLlmsTxt
async function writeLlmsTxt(docs: GeneratedDoc[], outputDir: string, options: { projectName?: string; description?: string } = {}): Promise<void>
Use writeLlmsTxt to generate an llms.txt file from your API docs, making your documentation discoverable and parseable by AI assistants and answer engines.
After running skrypt generate, call this to produce a standards-compliant llms.txt alongside your MDX output. This is the final step in a generation pipeline when you want LLMs like ChatGPT, Perplexity, or Claude to accurately answer questions about your API.
The output follows the llmstxt.org convention — a structured plaintext summary of your project's API surface, optimized for LLM context windows rather than human browsers.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
docs | GeneratedDoc[] | Yes | The documentation objects returned by your generation step — pass the same array you wrote to MDX. |
outputDir | string | Yes | Directory where llms.txt will be written. Should match the output directory used in your generation step. |
options.projectName | string | No | Name that appears at the top of the file as the API identifier. Defaults to "API" if omitted. |
options.description | string | No | One-sentence summary of your project, placed directly under the project name to give LLMs immediate context. |
Returns
Returns Promise<void>. The file is written to {outputDir}/llms.txt. No return value to handle — check the output directory for the generated file, then deploy it to your doc site's root so it's reachable at https://yourdocs.com/llms.txt.
Heads up
- The
outputDirmust already exist or be creatable — this function writes into it but won't fail silently if permissions are wrong. Wrap in try/catch to surface filesystem errors.- Deploy
llms.txtat your domain root, not nested under a path, for answer engines to find it via the standard discovery convention.
Example:
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
// Inline types — do not import from autodocs
interface GeneratedDoc {
name: string;
signature: string;
description: string;
parameters: { name: string; type: string; description: string }[];
returns: string;
examples: string[];
filePath: string;
}
// Inline implementation matching llmstxt.org convention
async function writeLlmsTxt(
docs: GeneratedDoc[],
outputDir: string,
options: { projectName?: string; description?: string } = {}
): Promise<void> {
const projectName = options.projectName || "API";
const description = options.description || "";
const lines: string[] = [
`# ${projectName}`,
"",
...(description ? [description, ""] : []),
"## API Reference",
"",
];
for (const doc of docs) {
lines.push(`### ${doc.name}`);
lines.push("");
lines.push(`\`\`\`ts`);
lines.push(doc.signature);
lines.push(`\`\`\``);
lines.push("");
lines.push(doc.description);
lines.push("");
}
if (!existsSync(outputDir)) {
await mkdir(outputDir, { recursive: true });
}
await writeFile(join(outputDir, "llms.txt"), lines.join("\n"), "utf-8");
}
// Realistic generated docs from a payment SDK
const generatedDocs: GeneratedDoc[] = [
{
name: "createCharge",
signature: "async function createCharge(amount: number, currency: string, source: string): Promise<Charge>",
description: "Creates a new charge against a payment source. Returns a Charge object with a status of 'pending' until confirmed by the payment network.",
parameters: [
{ name: "amount", type: "number", description: "Amount in smallest currency unit (e.g. cents)" },
{ name: "currency", type: "string", description: "ISO 4217 currency code" },
{ name: "source", type: "string", description: "Payment source token from tokenization step" },
],
returns: "Promise<Charge>",
examples: [],
filePath: "src/charges.ts",
},
{
name: "refundCharge",
signature: "async function refundCharge(chargeId: string, amount?: number): Promise<Refund>",
description: "Issues a full or partial refund for a completed charge. Partial refunds require the amount parameter.",
parameters: [
{ name: "chargeId", type: "string", description: "ID of the charge to refund" },
{ name: "amount", type: "number", description: "Optional partial refund amount in cents" },
],
returns: "Promise<Refund>",
examples: [],
filePath: "src/refunds.ts",
},
];
async function main() {
const outputDir = "./docs-output";
try {
await writeLlmsTxt(generatedDocs, outputDir, {
projectName: "Payments SDK",
description: "Unified API for processing charges, refunds, and subscriptions across payment providers.",
});
console.log(`llms.txt written to ${outputDir}/llms.txt`);
// Deploy this file to https://yourproject.com/llms.txt
// so AI assistants can discover your API surface automatically.
} catch (err) {
console.error("Failed to write llms.txt:", err);
process.exit(1);
}
}
main();
writeDocsToDirectory
async function writeDocsToDirectory(results: FileGenerationResult[], outputDir: string, sourceDir: string, options?: { force?: boolean }): Promise<{ filesWritten: number; totalDocs: number }>
Use writeDocsToDirectory to persist your generated documentation to disk, turning in-memory FileGenerationResult objects into organized MDX files ready for your doc site.
Call this as the final step after generating docs with skrypt generate — it takes the results array from the generation pipeline and writes each file to the correct output path, mirroring your source directory structure.
It resolves output paths relative to sourceDir, so the folder hierarchy in your docs matches your codebase. If a file already exists, it skips writing unless you pass force: true.
| Name | Type | Required | Description |
|---|---|---|---|
results | FileGenerationResult[] | Yes | The array of generated documentation objects returned by the generation pipeline. Each entry maps to one source file. |
outputDir | string | Yes | Directory where MDX files will be written. Created automatically if it doesn't exist. |
sourceDir | string | Yes | Root of the source code that was scanned — used to compute relative paths so your docs folder structure mirrors your source tree. |
options.force | boolean | No | When true, overwrites existing doc files. Defaults to false, skipping files that already exist on disk. |
Returns { filesWritten, totalDocs } — filesWritten is the number of files actually written to disk (skipped files don't count), and totalDocs is the total number of documented items across all files. Use filesWritten to confirm output and surface it in CI logs or progress indicators.
Heads up:
filesWrittenwill be less thanresults.lengthif files already exist andforceisfalse— this is expected behavior on incremental runs, not an error.sourceDirandoutputDirshould be absolute paths or consistently relative to the same working directory, otherwise path mirroring will produce unexpected folder structures.
Example:
import { mkdir, writeFile, readdir } from "fs/promises";
import { existsSync } from "fs";
import { dirname, join, relative, resolve } from "path";
// Inline types — do not import from autodocs
interface GeneratedDoc {
name: string;
description: string;
signature: string;
}
interface FileGenerationResult {
sourceFile: string;
docs: GeneratedDoc[];
mdxContent: string;
}
// Inline implementation of writeDocsToDirectory
async function writeDocsToDirectory(
results: FileGenerationResult[],
outputDir: string,
sourceDir: string,
options?: { force?: boolean }
): Promise<{ filesWritten: number; totalDocs: number }> {
const force = options?.force ?? false;
let filesWritten = 0;
let totalDocs = 0;
for (const result of results) {
const relativePath = relative(resolve(sourceDir), resolve(result.sourceFile));
const outputPath = join(outputDir, relativePath.replace(/\.(ts|py|go|rs)$/, ".mdx"));
const outputFileDir = dirname(outputPath);
await mkdir(outputFileDir, { recursive: true });
if (!force && existsSync(outputPath)) {
totalDocs += result.docs.length;
continue;
}
await writeFile(outputPath, result.mdxContent, "utf-8");
filesWritten++;
totalDocs += result.docs.length;
}
return { filesWritten, totalDocs };
}
// Realistic mock generation results
const results: FileGenerationResult[] = [
{
sourceFile: "/projects/my-api/src/auth/tokens.ts",
docs: [
{ name: "createToken", description: "Creates a signed JWT.", signature: "function createToken(userId: string): string" },
{ name: "verifyToken", description: "Verifies a JWT.", signature: "function verifyToken(token: string): Payload" },
],
mdxContent: "# auth/tokens\n\n## createToken\n\nCreates a signed JWT.\n\n## verifyToken\n\nVerifies a JWT.\n",
},
{
sourceFile: "/projects/my-api/src/users/profile.ts",
docs: [
{ name: "getProfile", description: "Fetches a user profile by ID.", signature: "async function getProfile(userId: string): Promise<Profile>" },
],
mdxContent: "# users/profile\n\n## getProfile\n\nFetches a user profile by ID.\n",
},
];
async function main() {
try {
const { filesWritten, totalDocs } = await writeDocsToDirectory(
results,
"./content/docs",
"/projects/my-api/src",
{ force: true }
);
console.log(`✔ Wrote ${filesWritten} file(s) covering ${totalDocs} documented items.`);
// Expected output:
// ✔ Wrote 2 file(s) covering 3 documented items.
} catch (err) {
console.error("Failed to write docs:", err);
process.exit(1);
}
}
main();
groupDocsByFile
function groupDocsByFile(docs: GeneratedDoc[]): FileGenerationResult[]
Use groupDocsByFile to organize a flat list of generated documentation entries into file-grouped bundles, ready for writing to disk or further processing.
Reach for this after running your AI doc generation step, when you need to consolidate multiple GeneratedDoc entries that originated from the same source file before rendering or saving them. It's the bridge between raw generated docs and the file-writing stage of a skrypt generate pipeline.
Each GeneratedDoc carries a filePath on its element — this function uses that path as the grouping key, collecting all docs that share a file into a single FileGenerationResult. The result preserves the one-to-many relationship between source files and their documented exports.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
docs | GeneratedDoc[] | Yes | Flat array of generated documentation entries, typically the output of a doc generation pass. Each entry must have element.filePath set — entries with the same path are merged into one result. |
Returns
Returns a FileGenerationResult[] where each element represents one source file and contains all the docs generated from it. Pass this array to your file-writing logic to emit one MDX (or Markdown) file per source file, or filter it to process specific files selectively.
Heads up
- Grouping is keyed on the exact string value of
element.filePath. If the same file appears with inconsistent paths (e.g.,./src/auth.tsvssrc/auth.ts), it will produce duplicate groups — normalize paths before calling this function.
Example:
// Inline types — do not import from autodocs
type ElementKind = "function" | "class" | "interface" | "type";
interface CodeElement {
name: string;
filePath: string;
kind: ElementKind;
signature: string;
}
interface GeneratedDoc {
element: CodeElement;
description: string;
params: Record<string, string>;
returns: string;
}
interface FileGenerationResult {
filePath: string;
docs: GeneratedDoc[];
}
function groupDocsByFile(docs: GeneratedDoc[]): FileGenerationResult[] {
const byFile = new Map<string, GeneratedDoc[]>();
for (const doc of docs) {
const file = doc.element.filePath;
if (!byFile.has(file)) {
byFile.set(file, []);
}
byFile.get(file)!.push(doc);
}
return Array.from(byFile.entries()).map(([filePath, docs]) => ({
filePath,
docs,
}));
}
// Simulate the output of a doc generation pass across two source files
const generatedDocs: GeneratedDoc[] = [
{
element: {
name: "createUser",
filePath: "src/users/service.ts",
kind: "function",
signature: "function createUser(email: string, role: string): Promise<User>",
},
description: "Creates a new user account and sends a welcome email.",
params: { email: "User's email address", role: "Assigned role: admin | member" },
returns: "The newly created User object with a generated ID.",
},
{
element: {
name: "deleteUser",
filePath: "src/users/service.ts",
kind: "function",
signature: "function deleteUser(userId: string): Promise<void>",
},
description: "Permanently deletes a user and revokes all active sessions.",
params: { userId: "UUID of the user to delete" },
returns: "Resolves when deletion is complete.",
},
{
element: {
name: "hashPassword",
filePath: "src/auth/crypto.ts",
kind: "function",
signature: "function hashPassword(plain: string): string",
},
description: "Hashes a plaintext password using bcrypt with a cost factor of 12.",
params: { plain: "The plaintext password to hash" },
returns: "A bcrypt hash string safe to store in the database.",
},
];
const grouped = groupDocsByFile(generatedDocs);
for (const result of grouped) {
console.log(`\nFile: ${result.filePath} (${result.docs.length} doc(s))`);
for (const doc of result.docs) {
console.log(` - ${doc.element.name}: ${doc.description}`);
}
}
// Expected output:
// File: src/users/service.ts (2 doc(s))
// - createUser: Creates a new user account and sends a welcome email.
// - deleteUser: Permanently deletes a user and revokes all active sessions.
//
// File: src/auth/crypto.ts (1 doc(s))
// - hashPassword: Hashes a plaintext password using bcrypt with a cost factor of 12.
writeDocsByTopic
async function writeDocsByTopic(docs: GeneratedDoc[], outputDir: string): Promise<{ filesWritten: number; totalDocs: number; topics: Topic[] }>
Use writeDocsByTopic to organize generated documentation into concept-based MDX files rather than mirroring your source file structure.
Reach for this after running your AI generation step when you want readers to navigate docs by what something does (authentication, pagination, webhooks) rather than where it lives in the codebase. It's the programmatic equivalent of skrypt generate --by-topic.
The function groups your flat array of GeneratedDoc objects into inferred topics, then writes one MDX file per topic into outputDir. Docs that don't belong to a clear topic are collected into a catch-all file so nothing is silently dropped.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
docs | GeneratedDoc[] | Yes | The documentation objects returned by your generation step — pass the full array so the topic-grouping algorithm has enough signal to cluster related items accurately. |
outputDir | string | Yes | Directory where topic MDX files are written. Created automatically if it doesn't exist. Use the same path you'd pass to -o in the CLI. |
Returns
Returns a Promise resolving to a summary object. Use filesWritten to confirm every topic produced a file, totalDocs to verify no entries were silently skipped, and topics to build a sidebar manifest or validate the groupings before committing the output.
Heads up
totalDocsreflects how manyGeneratedDocentries were distributed across topics — if it's lower thandocs.length, some entries were filtered out during grouping (usually due to missing or empty content fields).- Writing to an existing
outputDirwill overwrite files whose topic names match a previous run. Clear the directory first if you're regenerating from scratch to avoid stale topic files.
Example:
import { writeFile, mkdir } from "fs/promises";
import { join } from "path";
// Inline types — do not import from autodocs
interface GeneratedDoc {
name: string;
description: string;
signature: string;
topic: string;
parameters: { name: string; type: string; description: string }[];
returns: string;
example: string;
}
interface Topic {
name: string;
slug: string;
docs: GeneratedDoc[];
}
interface WriteResult {
filesWritten: number;
totalDocs: number;
topics: Topic[];
}
// Minimal self-contained implementation matching writeDocsByTopic's contract
async function writeDocsByTopic(
docs: GeneratedDoc[],
outputDir: string
): Promise<WriteResult> {
await mkdir(outputDir, { recursive: true });
const topicMap = new Map<string, GeneratedDoc[]>();
for (const doc of docs) {
const key = doc.topic || "general";
if (!topicMap.has(key)) topicMap.set(key, []);
topicMap.get(key)!.push(doc);
}
const topics: Topic[] = [];
let filesWritten = 0;
for (const [name, topicDocs] of topicMap) {
const slug = name.toLowerCase().replace(/\s+/g, "-");
const content = [
`# ${name}`,
"",
...topicDocs.flatMap((d) => [
`## ${d.name}`,
"",
d.description,
"",
`\`\`\`ts`,
d.signature,
`\`\`\``,
"",
]),
].join("\n");
await writeFile(join(outputDir, `${slug}.mdx`), content, "utf-8");
filesWritten++;
topics.push({ name, slug, docs: topicDocs });
}
return { filesWritten, totalDocs: docs.length, topics };
}
// Realistic generated docs from a payments SDK
const generatedDocs: GeneratedDoc[] = [
{
name: "createPaymentIntent",
description: "Creates a new PaymentIntent to begin a payment flow.",
signature: "function createPaymentIntent(amount: number, currency: string): Promise<PaymentIntent>",
topic: "Payments",
parameters: [{ name: "amount", type: "number", description: "Amount in cents" }],
returns: "PaymentIntent object",
example: "createPaymentIntent(4999, 'usd')",
},
{
name: "refundCharge",
description: "Issues a full or partial refund against a completed charge.",
signature: "function refundCharge(chargeId: string, amount?: number): Promise<Refund>",
topic: "Payments",
parameters: [{ name: "chargeId", type: "string", description: "ID of the charge to refund" }],
returns: "Refund object",
example: "refundCharge('ch_3Pu8Lz2eZvKYlo2C', 2000)",
},
{
name: "createWebhookEndpoint",
description: "Registers a URL to receive real-time event notifications.",
signature: "function createWebhookEndpoint(url: string, events: string[]): Promise<WebhookEndpoint>",
topic: "Webhooks",
parameters: [{ name: "url", type: "string", description: "Public HTTPS URL to receive events" }],
returns: "WebhookEndpoint object",
example: "createWebhookEndpoint('https://api.acme.com/hooks', ['payment.succeeded'])",
},
];
async function main() {
try {
const result = await writeDocsByTopic(generatedDocs, "./docs/by-topic");
console.log(`Files written : ${result.filesWritten}`);
console.log(`Total docs : ${result.totalDocs}`);
console.log("Topics:");
for (const topic of result.topics) {
console.log(` [${topic.slug}] ${topic.name} — ${topic.docs.length} doc(s)`);
}
// Expected output:
// Files written : 2
// Total docs : 3
// Topics:
// [payments] Payments — 2 doc(s)
// [webhooks] Webhooks — 1 doc(s)
} catch (err) {
console.error("Failed to write docs by topic:", err);
process.exit(1);
}
}
main();