diff --git a/.changeset/great-rocks-read.md b/.changeset/great-rocks-read.md new file mode 100644 index 000000000..9bfb03818 --- /dev/null +++ b/.changeset/great-rocks-read.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +Bundle version-matched Stagehand docs and agent instructions in the published package. diff --git a/packages/core/AGENTS.md b/packages/core/AGENTS.md new file mode 100644 index 000000000..92981e1c4 --- /dev/null +++ b/packages/core/AGENTS.md @@ -0,0 +1,12 @@ +# Stagehand Package + +This package ships version-matched documentation for coding agents. + +Before changing code that uses Stagehand, inspect the relevant local docs: + +```bash +rg "Stagehand" node_modules/@browserbasehq/stagehand/dist/docs +rg "stagehand\\.act|stagehand\\.extract|stagehand\\.observe|stagehand\\.agent" node_modules/@browserbasehq/stagehand/dist/docs +``` + +Start with `dist/docs/index.md`, then read the specific reference or guide for the API you are using. diff --git a/packages/core/package.json b/packages/core/package.json index 2b5d2cfb3..5850ca60a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,9 +33,10 @@ "scripts": { "gen-version": "tsx scripts/gen-version.ts", "build-dom-scripts": "tsx scripts/build-dom-scripts.ts", + "build-docs": "tsx scripts/build-docs.ts", "build:cjs": "tsx scripts/build-cjs.ts", "build:esm": "tsx scripts/build-esm.ts", - "build": "pnpm --filter @browserbasehq/stagehand run --parallel \"/^build:(esm|cjs)$/\"", + "build": "pnpm --filter @browserbasehq/stagehand run --parallel \"/^build:(esm|cjs)$/\" && pnpm run build-docs", "example": "node --import tsx -e \"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\.\\//,'').replace(/\\.ts$/i,''); import('node:path').then(path=>import(new URL(path.resolve('examples', n + '.ts'), 'file:')));\" --", "test": "pnpm -w --dir ../.. exec turbo run test:core test:e2e --filter=@browserbasehq/stagehand --", "test:core": "tsx scripts/test-core.ts", @@ -47,7 +48,9 @@ }, "files": [ "dist/esm", - "dist/cjs" + "dist/cjs", + "dist/docs", + "AGENTS.md" ], "keywords": [ "ai", diff --git a/packages/core/scripts/build-docs.ts b/packages/core/scripts/build-docs.ts new file mode 100644 index 000000000..20ee886e9 --- /dev/null +++ b/packages/core/scripts/build-docs.ts @@ -0,0 +1,211 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +type DocEntry = { + outputPath: string; + sourcePath: string; + title: string; + description?: string; +}; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const packageRoot = path.resolve(scriptDir, ".."); +const repoRoot = path.resolve(packageRoot, "../.."); +const docsSourceRoot = path.join(repoRoot, "packages/docs/v3"); +const docsOutputRoot = path.join(packageRoot, "dist/docs"); +const packageJsonPath = path.join(packageRoot, "package.json"); + +const keyDocs = [ + "v3/first-steps/quickstart.md", + "v3/first-steps/installation.md", + "v3/references/stagehand.md", + "v3/references/act.md", + "v3/references/extract.md", + "v3/references/observe.md", + "v3/references/agent.md", + "v3/references/page.md", + "v3/references/context.md", + "v3/configuration/models.md", + "v3/best-practices/caching.md", + "v3/best-practices/prompting-best-practices.md", +]; + +async function collectMdxFiles(directory: string): Promise { + const entries = await fs.readdir(directory, { withFileTypes: true }); + const files = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + return collectMdxFiles(entryPath); + } + + return entry.isFile() && entry.name.endsWith(".mdx") ? [entryPath] : []; + }), + ); + + return files.flat().sort(); +} + +function readFrontmatter(content: string): { + title?: string; + description?: string; +} { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) { + return {}; + } + + const fields: Record = {}; + for (const line of match[1].split("\n")) { + const field = line.match(/^([A-Za-z0-9_-]+):\s*['"]?(.*?)['"]?\s*$/); + if (field) { + fields[field[1]] = field[2]; + } + } + + return { + title: fields.title, + description: fields.description, + }; +} + +function titleFromPath(relativePath: string): string { + const basename = path.basename(relativePath, ".mdx"); + return basename + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +function convertMdxToMarkdown(content: string): string { + return content + .replace(/^import\s+.*?;\s*$/gm, "") + .replace(/^\s*\s*$/gm, "") + .replace(/^\s*\s*<\/V3Banner>\s*$/gm, "") + .replace(/\n{3,}/g, "\n\n") + .trimEnd() + .concat("\n"); +} + +function tryGit(args: string[]): string | undefined { + try { + return execFileSync("git", args, { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return undefined; + } +} + +async function writeIndex(docs: DocEntry[]) { + const packageJson = JSON.parse( + await fs.readFile(packageJsonPath, "utf8"), + ) as { + version: string; + }; + const commit = tryGit(["rev-parse", "--short", "HEAD"]); + const sourceDate = tryGit([ + "log", + "-1", + "--format=%cI", + "--", + "packages/docs/v3", + ]); + + const docByPath = new Map(docs.map((doc) => [doc.outputPath, doc])); + const keyDocLines = keyDocs + .filter((docPath) => docByPath.has(docPath)) + .map((docPath) => { + const doc = docByPath.get(docPath); + return `- [${doc?.title ?? docPath}](${docPath})`; + }) + .join("\n"); + + const allDocLines = docs + .map((doc) => { + const description = doc.description ? ` - ${doc.description}` : ""; + return `- [${doc.title}](${doc.outputPath})${description}`; + }) + .join("\n"); + + const metadata = [ + `- Package: \`@browserbasehq/stagehand@${packageJson.version}\``, + `- Generated from: \`packages/docs/v3/**/*.mdx\``, + commit ? `- Source commit: \`${commit}\`` : undefined, + sourceDate + ? `- Latest docs source commit date: \`${sourceDate}\`` + : undefined, + ] + .filter(Boolean) + .join("\n"); + + const content = `# Stagehand Local Docs + +These docs ship inside the installed \`@browserbasehq/stagehand\` package so coding agents and developers can inspect version-matched Stagehand guidance without leaving the project. + +## Metadata + +${metadata} + +## How To Search + +\`\`\`bash +rg "Stagehand" node_modules/@browserbasehq/stagehand/dist/docs +rg "stagehand\\.act|stagehand\\.extract|stagehand\\.observe|stagehand\\.agent" node_modules/@browserbasehq/stagehand/dist/docs +\`\`\` + +## Start Here + +${keyDocLines} + +## All Docs + +${allDocLines} +`; + + await fs.writeFile(path.join(docsOutputRoot, "index.md"), content, "utf8"); +} + +async function main() { + await fs.rm(docsOutputRoot, { recursive: true, force: true }); + await fs.mkdir(path.join(docsOutputRoot, "v3"), { recursive: true }); + + const mdxFiles = await collectMdxFiles(docsSourceRoot); + const docs: DocEntry[] = []; + + for (const sourcePath of mdxFiles) { + const relativeSourcePath = path.relative(docsSourceRoot, sourcePath); + const relativeOutputPath = path + .join("v3", relativeSourcePath) + .replace(/\.mdx$/, ".md") + .replaceAll(path.sep, "/"); + const outputPath = path.join(docsOutputRoot, relativeOutputPath); + const content = await fs.readFile(sourcePath, "utf8"); + const frontmatter = readFrontmatter(content); + + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await fs.writeFile(outputPath, convertMdxToMarkdown(content), "utf8"); + + docs.push({ + outputPath: relativeOutputPath, + sourcePath: path.relative(repoRoot, sourcePath).replaceAll(path.sep, "/"), + title: frontmatter.title ?? titleFromPath(relativeSourcePath), + description: frontmatter.description, + }); + } + + await writeIndex(docs); + + console.log( + `Generated ${docs.length + 1} Stagehand docs in ${docsOutputRoot}`, + ); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/turbo.json b/turbo.json index 578d247f5..b0131835d 100644 --- a/turbo.json +++ b/turbo.json @@ -40,6 +40,9 @@ "inputs": [ "**/*.ts", "**/*.js", + "$TURBO_ROOT$/packages/docs/docs.json", + "$TURBO_ROOT$/packages/docs/snippets/**/*.mdx", + "$TURBO_ROOT$/packages/docs/v3/**/*.mdx", "package.json", "tsconfig.json", "tsconfig.*.json", @@ -130,6 +133,9 @@ "inputs": [ "**/*.ts", "**/*.js", + "$TURBO_ROOT$/packages/docs/docs.json", + "$TURBO_ROOT$/packages/docs/snippets/**/*.mdx", + "$TURBO_ROOT$/packages/docs/v3/**/*.mdx", "package.json", "tsconfig.json", "tsconfig.*.json", @@ -151,6 +157,9 @@ "inputs": [ "**/*.ts", "**/*.js", + "$TURBO_ROOT$/packages/docs/docs.json", + "$TURBO_ROOT$/packages/docs/snippets/**/*.mdx", + "$TURBO_ROOT$/packages/docs/v3/**/*.mdx", "package.json", "tsconfig.json", "tsconfig.*.json",