feat: expand api snapshots and schema tooling

This commit is contained in:
Nathan Flurry 2026-01-26 00:13:17 -08:00
parent ee014b0838
commit 011ca27287
72 changed files with 29480 additions and 1081 deletions

View file

@ -3,7 +3,7 @@ import { fetchWithCache } from "./cache.js";
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
import type { JSONSchema7 } from "json-schema";
const AMP_DOCS_URL = "https://ampcode.com/manual/appendix";
const AMP_DOCS_URL = "https://ampcode.com/manual/appendix?preview#message-schema";
// Key types we want to extract
const TARGET_TYPES = ["StreamJSONMessage", "AmpOptions", "PermissionRule", "Message", "ToolCall"];

View file

@ -0,0 +1,11 @@
import { collectFromCli } from "./claude-event-types.js";
const promptArg = process.argv.slice(2).find((arg) => arg.startsWith("--prompt="));
const timeoutArg = process.argv.slice(2).find((arg) => arg.startsWith("--timeoutMs="));
const prompt = promptArg?.split("=")[1] ?? "Reply with exactly OK.";
const timeoutMs = timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000;
collectFromCli(prompt, timeoutMs).then((result) => {
console.log(JSON.stringify(result, null, 2));
});

View file

@ -0,0 +1,8 @@
import { collectFromDocs } from "./claude-event-types.js";
const urlsArg = process.argv.slice(2).find((arg) => arg.startsWith("--urls="));
const urls = urlsArg ? urlsArg.split("=")[1]!.split(",") : undefined;
collectFromDocs(urls ?? []).then((result) => {
console.log(JSON.stringify(result, null, 2));
});

View file

@ -0,0 +1,4 @@
import { collectFromSdkTypes } from "./claude-event-types.js";
const result = collectFromSdkTypes();
console.log(JSON.stringify(result, null, 2));

View file

@ -0,0 +1,338 @@
import { readFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import ts from "typescript";
import { load } from "cheerio";
type SourceResult = {
source: string;
types: string[];
details?: Record<string, string[]>;
error?: string;
};
const SDK_POSSIBLE_PATHS = [
"node_modules/@anthropic-ai/claude-code/sdk-tools.d.ts",
"node_modules/@anthropic-ai/claude-code/dist/index.d.ts",
"node_modules/@anthropic-ai/claude-code/dist/types.d.ts",
"node_modules/@anthropic-ai/claude-code/index.d.ts",
];
const DEFAULT_DOC_URLS = [
"https://platform.claude.com/docs/en/messages-streaming",
"https://platform.claude.com/docs/en/api/messages-streaming",
"https://docs.anthropic.com/claude/reference/messages-streaming",
"https://docs.anthropic.com/claude/reference/messages-streaming#events",
"https://docs.anthropic.com/claude/docs/messages-streaming",
];
function moduleDir(): string {
const metaDir = (import.meta as { dirname?: string }).dirname;
if (typeof metaDir === "string") {
return metaDir;
}
return dirname(fileURLToPath(import.meta.url));
}
function findSdkTypesPath(): string | null {
const resourceDir = join(moduleDir(), "..");
const repoRoot = join(moduleDir(), "..", "..", "..");
const searchRoots = [resourceDir, repoRoot];
for (const root of searchRoots) {
for (const relativePath of SDK_POSSIBLE_PATHS) {
const fullPath = join(root, relativePath);
if (existsSync(fullPath)) {
return fullPath;
}
}
}
return null;
}
function extractStringLiterals(node: ts.TypeNode): string[] {
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
return [node.literal.text];
}
if (ts.isUnionTypeNode(node)) {
return node.types.flatMap((typeNode) => extractStringLiterals(typeNode));
}
return [];
}
function containerName(node: ts.Node): string | null {
let current: ts.Node | undefined = node;
while (current) {
if (ts.isInterfaceDeclaration(current) && current.name) {
return current.name.text;
}
if (ts.isTypeAliasDeclaration(current) && current.name) {
return current.name.text;
}
current = current.parent;
}
return null;
}
function collectFromSdkTypes(): SourceResult {
const path = findSdkTypesPath();
if (!path) {
return { source: "sdk", types: [], error: "Claude SDK types not found" };
}
const content = readFileSync(path, "utf8");
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
const types = new Set<string>();
const details: Record<string, string[]> = {};
function visit(node: ts.Node): void {
if (ts.isPropertySignature(node)) {
const name = node.name && ts.isIdentifier(node.name) ? node.name.text : null;
if (name === "type" && node.type) {
const literals = extractStringLiterals(node.type);
if (literals.length > 0) {
const parentName = containerName(node) ?? "anonymous";
if (/Event|Stream|Message/i.test(parentName)) {
literals.forEach((value) => types.add(value));
details[parentName] = (details[parentName] ?? []).concat(literals);
}
}
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return { source: "sdk", types: Array.from(types).sort(), details };
}
function collectFromCli(prompt: string, timeoutMs: number): Promise<SourceResult> {
return new Promise((resolve) => {
const result: SourceResult = { source: "cli", types: [] };
const types = new Set<string>();
const denoGlobal = (globalThis as {
Deno?: {
which?: (cmd: string) => string | null;
Command?: new (
cmd: string,
options: { args: string[]; stdout: "piped"; stderr: "piped" },
) => { output: () => Promise<{ stdout: Uint8Array; stderr: Uint8Array; code: number }> };
};
}).Deno;
if (denoGlobal?.which && !denoGlobal.which("claude")) {
result.error = "claude binary not found in PATH";
resolve(result);
return;
}
if (denoGlobal?.Command) {
const command = new denoGlobal.Command("claude", {
args: ["--print", "--output-format", "stream-json", "--verbose", prompt],
stdout: "piped",
stderr: "piped",
});
try {
command
.output()
.then(({ stdout, stderr, code }) => {
const text = new TextDecoder().decode(stdout);
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const value = JSON.parse(trimmed);
if (value && typeof value.type === "string") {
types.add(value.type);
}
} catch {
// ignore non-json
}
}
result.types = Array.from(types).sort();
if (code !== 0) {
result.error =
new TextDecoder().decode(stderr).trim() ||
`claude exited with code ${code}`;
}
resolve(result);
})
.catch((error) => {
result.error = error instanceof Error ? error.message : String(error);
resolve(result);
});
} catch (error) {
result.error = error instanceof Error ? error.message : String(error);
resolve(result);
}
return;
}
let child;
try {
child = spawn(
"claude",
["--print", "--output-format", "stream-json", "--verbose", prompt],
{ stdio: ["ignore", "pipe", "pipe"] },
);
} catch (error) {
result.error = error instanceof Error ? error.message : String(error);
resolve(result);
return;
}
if (!child.stdout || !child.stderr) {
result.error = "claude stdout/stderr not available";
resolve(result);
return;
}
let stderr = "";
const timer = setTimeout(() => {
child.kill("SIGKILL");
}, timeoutMs);
child.stdout.on("data", (chunk) => {
const text = chunk.toString("utf8");
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const value = JSON.parse(trimmed);
if (value && typeof value.type === "string") {
types.add(value.type);
}
} catch {
// ignore non-json
}
}
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.on("close", (code) => {
clearTimeout(timer);
result.types = Array.from(types).sort();
if (code !== 0) {
result.error = stderr.trim() || `claude exited with code ${code}`;
}
resolve(result);
});
});
}
async function collectFromDocs(urls: string[]): Promise<SourceResult> {
if (typeof fetch !== "function") {
return { source: "docs", types: [], error: "fetch is not available in this runtime" };
}
const effectiveUrls = urls.length > 0 ? urls : DEFAULT_DOC_URLS;
const types = new Set<string>();
const extractFromText = (text: string) => {
const typeMatches = text.match(/\"type\"\\s*:\\s*\"([^\"]+)\"/g) ?? [];
for (const match of typeMatches) {
const value = match.split(":")[1]?.trim().replace(/^\"|\"$/g, "");
if (value) types.add(value);
}
const eventMatches = text.match(/event\\s*:\\s*([a-z_]+)/gi) ?? [];
for (const match of eventMatches) {
const value = match.split(":")[1]?.trim();
if (value) types.add(value);
}
};
for (const url of effectiveUrls) {
try {
const res = await fetch(url);
if (!res.ok) {
continue;
}
const html = await res.text();
const $ = load(html);
const blocks = $("pre, code")
.map((_, el) => $(el).text())
.get();
for (const block of blocks) {
extractFromText(block);
}
const nextData = $("#__NEXT_DATA__").text();
if (nextData) {
extractFromText(nextData);
}
extractFromText(html);
} catch {
// ignore per-url errors
}
}
return { source: "docs", types: Array.from(types).sort() };
}
type Args = {
source: "all" | "sdk" | "cli" | "docs";
prompt: string;
timeoutMs: number;
urls: string[];
json: boolean;
};
function parseArgs(): Args {
const args = process.argv.slice(2);
const sourceArg = args.find((arg) => arg.startsWith("--source="));
const promptArg = args.find((arg) => arg.startsWith("--prompt="));
const timeoutArg = args.find((arg) => arg.startsWith("--timeoutMs="));
const urlsArg = args.find((arg) => arg.startsWith("--urls="));
const json = args.includes("--json");
return {
source: (sourceArg?.split("=")[1] as Args["source"]) ?? "all",
prompt: promptArg?.split("=")[1] ?? "Reply with exactly OK.",
timeoutMs: timeoutArg ? Number(timeoutArg.split("=")[1]) : 20000,
urls: urlsArg ? urlsArg.split("=")[1]!.split(",") : DEFAULT_DOC_URLS,
json,
};
}
function summarize(results: SourceResult[]): void {
const counts = results.map((r) => ({ source: r.source, count: r.types.length }));
const max = Math.max(...counts.map((c) => c.count), 0);
const best = counts.filter((c) => c.count === max).map((c) => c.source);
const union = Array.from(
new Set(results.flatMap((r) => r.types))
).sort();
console.log("Claude event type extraction");
console.log("============================");
for (const result of results) {
console.log(`- ${result.source}: ${result.types.length} types${result.error ? " (error)" : ""}`);
}
console.log(`\nMost comprehensive: ${best.join(", ") || "none"}`);
console.log(`Union (${union.length}): ${union.join(", ")}`);
}
async function main(): Promise<void> {
const args = parseArgs();
const results: SourceResult[] = [];
if (args.source === "all" || args.source === "sdk") {
results.push(collectFromSdkTypes());
}
if (args.source === "all" || args.source === "cli") {
results.push(await collectFromCli(args.prompt, args.timeoutMs));
}
if (args.source === "all" || args.source === "docs") {
results.push(await collectFromDocs(args.urls));
}
if (args.json) {
console.log(JSON.stringify({ results }, null, 2));
return;
}
summarize(results);
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
export { collectFromCli, collectFromDocs, collectFromSdkTypes };

View file

@ -1,92 +1,43 @@
import { createGenerator, type Config } from "ts-json-schema-generator";
import { existsSync, readFileSync } from "fs";
import { join, dirname } from "path";
import { execSync } from "child_process";
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
import type { JSONSchema7 } from "json-schema";
// Try multiple possible paths for the SDK types
const POSSIBLE_PATHS = [
"node_modules/@anthropic-ai/claude-code/sdk-tools.d.ts",
"node_modules/@anthropic-ai/claude-code/dist/index.d.ts",
"node_modules/@anthropic-ai/claude-code/dist/types.d.ts",
"node_modules/@anthropic-ai/claude-code/index.d.ts",
];
// Key types we want to extract
const TARGET_TYPES = [
"ToolInputSchemas",
"AgentInput",
"BashInput",
"FileEditInput",
"FileReadInput",
"FileWriteInput",
"GlobInput",
"GrepInput",
"WebFetchInput",
"WebSearchInput",
"AskUserQuestionInput",
];
function findTypesPath(): string | null {
const baseDir = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
for (const relativePath of POSSIBLE_PATHS) {
const fullPath = join(baseDir, relativePath);
if (existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
export async function extractClaudeSchema(): Promise<NormalizedSchema> {
console.log("Extracting Claude Code SDK schema...");
const typesPath = findTypesPath();
if (!typesPath) {
console.log(" [warn] Claude Code SDK types not found, using fallback schema");
return createFallbackSchema();
}
console.log(` [found] ${typesPath}`);
const config: Config = {
path: typesPath,
tsconfig: join(import.meta.dirname, "..", "..", "resources", "agent-schemas", "tsconfig.json"),
type: "*",
skipTypeCheck: true,
topRef: false,
expose: "export",
jsDoc: "extended",
};
console.log("Extracting Claude Code schema via CLI...");
try {
const generator = createGenerator(config);
const schema = generator.createSchema(config.type);
// Run claude CLI with --json-schema flag to get the schema
const output = execSync("claude --output-format json --json-schema", {
encoding: "utf-8",
timeout: 30000,
stdio: ["pipe", "pipe", "pipe"],
});
// Parse the JSON output
const parsed = JSON.parse(output);
// Extract definitions from the schema
const definitions: Record<string, JSONSchema7> = {};
if (schema.definitions) {
for (const [name, def] of Object.entries(schema.definitions)) {
if (parsed.definitions) {
for (const [name, def] of Object.entries(parsed.definitions)) {
definitions[name] = def as JSONSchema7;
}
} else if (parsed.$defs) {
for (const [name, def] of Object.entries(parsed.$defs)) {
definitions[name] = def as JSONSchema7;
}
} else {
// The output might be a single schema, use it as the root
definitions["Schema"] = parsed as JSONSchema7;
}
// Verify target types exist
const found = TARGET_TYPES.filter((name) => definitions[name]);
const missing = TARGET_TYPES.filter((name) => !definitions[name]);
if (missing.length > 0) {
console.log(` [warn] Missing expected types: ${missing.join(", ")}`);
}
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
} catch (error) {
console.log(` [error] Schema generation failed: ${error}`);
const errorMessage = error instanceof Error ? error.message : String(error);
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
console.log(" [fallback] Using embedded schema definitions");
return createFallbackSchema();
}

View file

@ -1,88 +1,69 @@
import { createGenerator, type Config } from "ts-json-schema-generator";
import { existsSync } from "fs";
import { execSync } from "child_process";
import { existsSync, readFileSync, rmSync, readdirSync } from "fs";
import { join } from "path";
import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js";
import type { JSONSchema7 } from "json-schema";
// Try multiple possible paths for the SDK types
const POSSIBLE_PATHS = [
"node_modules/@openai/codex/dist/index.d.ts",
"node_modules/@openai/codex/dist/types.d.ts",
"node_modules/@openai/codex/index.d.ts",
];
// Key types we want to extract
const TARGET_TYPES = [
"ThreadEvent",
"ThreadItem",
"CodexOptions",
"ThreadOptions",
"Input",
"ResponseItem",
"FunctionCall",
"Message",
];
function findTypesPath(): string | null {
const baseDir = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
for (const relativePath of POSSIBLE_PATHS) {
const fullPath = join(baseDir, relativePath);
if (existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
export async function extractCodexSchema(): Promise<NormalizedSchema> {
console.log("Extracting Codex SDK schema...");
console.log("Extracting Codex schema via CLI...");
const typesPath = findTypesPath();
if (!typesPath) {
console.log(" [warn] Codex SDK types not found, using fallback schema");
return createFallbackSchema();
}
console.log(` [found] ${typesPath}`);
const config: Config = {
path: typesPath,
tsconfig: join(import.meta.dirname, "..", "..", "resources", "agent-schemas", "tsconfig.json"),
type: "*",
skipTypeCheck: true,
topRef: false,
expose: "export",
jsDoc: "extended",
};
const tempDir = join(import.meta.dirname, "..", ".temp-codex-schemas");
try {
const generator = createGenerator(config);
const schema = generator.createSchema(config.type);
// Run codex CLI to generate JSON schema
execSync(`codex app-server generate-json-schema --out "${tempDir}"`, {
encoding: "utf-8",
timeout: 30000,
stdio: ["pipe", "pipe", "pipe"],
});
// Read generated schema files from temp directory
const definitions: Record<string, JSONSchema7> = {};
if (schema.definitions) {
for (const [name, def] of Object.entries(schema.definitions)) {
definitions[name] = def as JSONSchema7;
if (existsSync(tempDir)) {
const files = readdirSync(tempDir).filter((f) => f.endsWith(".json"));
for (const file of files) {
const filePath = join(tempDir, file);
const content = readFileSync(filePath, "utf-8");
const schema = JSON.parse(content);
// Extract the name from the file (e.g., "ThreadEvent.json" -> "ThreadEvent")
const name = file.replace(".json", "");
if (schema.definitions) {
for (const [defName, def] of Object.entries(schema.definitions)) {
definitions[defName] = def as JSONSchema7;
}
} else if (schema.$defs) {
for (const [defName, def] of Object.entries(schema.$defs)) {
definitions[defName] = def as JSONSchema7;
}
} else {
definitions[name] = schema as JSONSchema7;
}
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true });
}
// Verify target types exist
const found = TARGET_TYPES.filter((name) => definitions[name]);
const missing = TARGET_TYPES.filter((name) => !definitions[name]);
if (missing.length > 0) {
console.log(` [warn] Missing expected types: ${missing.join(", ")}`);
if (Object.keys(definitions).length === 0) {
console.log(" [warn] No schemas extracted from CLI, using fallback");
return createFallbackSchema();
}
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
console.log(` [ok] Extracted ${Object.keys(definitions).length} types from CLI`);
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
} catch (error) {
console.log(` [error] Schema generation failed: ${error}`);
// Clean up temp directory on error
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
const errorMessage = error instanceof Error ? error.message : String(error);
console.log(` [warn] CLI extraction failed: ${errorMessage}`);
console.log(" [fallback] Using embedded schema definitions");
return createFallbackSchema();
}

View file

@ -6,8 +6,8 @@ import { extractCodexSchema } from "./codex.js";
import { extractAmpSchema } from "./amp.js";
import { validateSchema, type NormalizedSchema } from "./normalize.js";
const RESOURCE_DIR = join(import.meta.dirname, "..", "..", "resources", "agent-schemas");
const DIST_DIR = join(RESOURCE_DIR, "dist");
const RESOURCE_DIR = join(import.meta.dirname, "..");
const DIST_DIR = join(RESOURCE_DIR, "artifacts", "json-schema");
type AgentName = "opencode" | "claude" | "codex" | "amp";