feat: refresh docs and agent schema

This commit is contained in:
Nathan Flurry 2026-01-25 03:04:12 -08:00
parent a49ea094f3
commit 0fbf6272b1
39 changed files with 3127 additions and 1806 deletions

View file

@ -4,11 +4,11 @@
"type": "module",
"license": "Apache-2.0",
"scripts": {
"extract": "tsx src/index.ts",
"extract:opencode": "tsx src/index.ts --agent=opencode",
"extract:claude": "tsx src/index.ts --agent=claude",
"extract:codex": "tsx src/index.ts --agent=codex",
"extract:amp": "tsx src/index.ts --agent=amp"
"extract": "tsx ../../src/agents/index.ts",
"extract:opencode": "tsx ../../src/agents/index.ts --agent=opencode",
"extract:claude": "tsx ../../src/agents/index.ts --agent=claude",
"extract:codex": "tsx ../../src/agents/index.ts --agent=codex",
"extract:amp": "tsx ../../src/agents/index.ts --agent=amp"
},
"dependencies": {
"ts-json-schema-generator": "^2.4.0",

View file

@ -1,271 +0,0 @@
import * as cheerio from "cheerio";
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";
// Key types we want to extract
const TARGET_TYPES = ["StreamJSONMessage", "AmpOptions", "PermissionRule", "Message", "ToolCall"];
export async function extractAmpSchema(): Promise<NormalizedSchema> {
console.log("Extracting AMP schema from documentation...");
try {
const html = await fetchWithCache(AMP_DOCS_URL);
const $ = cheerio.load(html);
// Find TypeScript code blocks
const codeBlocks: string[] = [];
$("pre code").each((_, el) => {
const code = $(el).text();
// Look for TypeScript interface/type definitions
if (
code.includes("interface ") ||
code.includes("type ") ||
code.includes(": {") ||
code.includes("export ")
) {
codeBlocks.push(code);
}
});
if (codeBlocks.length === 0) {
console.log(" [warn] No TypeScript code blocks found, using fallback schema");
return createFallbackSchema();
}
console.log(` [found] ${codeBlocks.length} code blocks`);
// Parse TypeScript definitions into schemas
const definitions = parseTypeScriptToSchema(codeBlocks.join("\n"));
// 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 types extracted, using fallback schema");
return createFallbackSchema();
}
console.log(` [ok] Extracted ${Object.keys(definitions).length} types (${found.length} target types)`);
return createNormalizedSchema("amp", "AMP Code SDK Schema", definitions);
} catch (error) {
console.log(` [error] Failed to fetch docs: ${error}`);
console.log(" [fallback] Using embedded schema definitions");
return createFallbackSchema();
}
}
function parseTypeScriptToSchema(code: string): Record<string, JSONSchema7> {
const definitions: Record<string, JSONSchema7> = {};
// Match interface definitions
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)\s*(?:extends\s+[\w,\s]+)?\s*\{([^}]+)\}/g;
let match;
while ((match = interfaceRegex.exec(code)) !== null) {
const [, name, body] = match;
definitions[name] = parseInterfaceBody(body);
}
// Match type definitions (simple object types)
const typeRegex = /(?:export\s+)?type\s+(\w+)\s*=\s*\{([^}]+)\}/g;
while ((match = typeRegex.exec(code)) !== null) {
const [, name, body] = match;
definitions[name] = parseInterfaceBody(body);
}
// Match union type definitions
const unionRegex = /(?:export\s+)?type\s+(\w+)\s*=\s*([^;{]+);/g;
while ((match = unionRegex.exec(code)) !== null) {
const [, name, body] = match;
if (body.includes("|")) {
definitions[name] = parseUnionType(body);
}
}
return definitions;
}
function parseInterfaceBody(body: string): JSONSchema7 {
const properties: Record<string, JSONSchema7> = {};
const required: string[] = [];
// Match property definitions
const propRegex = /(\w+)(\?)?:\s*([^;]+);/g;
let match;
while ((match = propRegex.exec(body)) !== null) {
const [, propName, optional, propType] = match;
properties[propName] = typeToSchema(propType.trim());
if (!optional) {
required.push(propName);
}
}
const schema: JSONSchema7 = {
type: "object",
properties,
};
if (required.length > 0) {
schema.required = required;
}
return schema;
}
function typeToSchema(tsType: string): JSONSchema7 {
// Handle union types
if (tsType.includes("|")) {
return parseUnionType(tsType);
}
// Handle array types
if (tsType.endsWith("[]")) {
const itemType = tsType.slice(0, -2);
return {
type: "array",
items: typeToSchema(itemType),
};
}
// Handle Array<T>
const arrayMatch = tsType.match(/^Array<(.+)>$/);
if (arrayMatch) {
return {
type: "array",
items: typeToSchema(arrayMatch[1]),
};
}
// Handle basic types
switch (tsType) {
case "string":
return { type: "string" };
case "number":
return { type: "number" };
case "boolean":
return { type: "boolean" };
case "null":
return { type: "null" };
case "any":
case "unknown":
return {};
case "object":
return { type: "object" };
default:
// Could be a reference to another type
if (/^[A-Z]/.test(tsType)) {
return { $ref: `#/definitions/${tsType}` };
}
// String literal
if (tsType.startsWith('"') || tsType.startsWith("'")) {
return { type: "string", const: tsType.slice(1, -1) };
}
return { type: "string" };
}
}
function parseUnionType(unionStr: string): JSONSchema7 {
const parts = unionStr.split("|").map((p) => p.trim());
// Check if it's a string literal union
const allStringLiterals = parts.every((p) => p.startsWith('"') || p.startsWith("'"));
if (allStringLiterals) {
return {
type: "string",
enum: parts.map((p) => p.slice(1, -1)),
};
}
// General union
return {
oneOf: parts.map((p) => typeToSchema(p)),
};
}
function createFallbackSchema(): NormalizedSchema {
// Fallback schema based on AMP documentation structure
const definitions: Record<string, JSONSchema7> = {
StreamJSONMessage: {
type: "object",
properties: {
type: {
type: "string",
enum: ["message", "tool_call", "tool_result", "error", "done"],
},
id: { type: "string" },
content: { type: "string" },
tool_call: { $ref: "#/definitions/ToolCall" },
error: { type: "string" },
},
required: ["type"],
},
AmpOptions: {
type: "object",
properties: {
model: { type: "string" },
apiKey: { type: "string" },
baseURL: { type: "string" },
maxTokens: { type: "number" },
temperature: { type: "number" },
systemPrompt: { type: "string" },
tools: { type: "array", items: { type: "object" } },
workingDirectory: { type: "string" },
permissionRules: {
type: "array",
items: { $ref: "#/definitions/PermissionRule" },
},
},
},
PermissionRule: {
type: "object",
properties: {
tool: { type: "string" },
action: { type: "string", enum: ["allow", "deny", "ask"] },
pattern: { type: "string" },
description: { type: "string" },
},
required: ["tool", "action"],
},
Message: {
type: "object",
properties: {
role: { type: "string", enum: ["user", "assistant", "system"] },
content: { type: "string" },
tool_calls: {
type: "array",
items: { $ref: "#/definitions/ToolCall" },
},
},
required: ["role", "content"],
},
ToolCall: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
arguments: {
oneOf: [{ type: "string" }, { type: "object" }],
},
},
required: ["id", "name", "arguments"],
},
};
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
return createNormalizedSchema("amp", "AMP Code SDK Schema", definitions);
}

View file

@ -1,94 +0,0 @@
import { createHash } from "crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
import { join } from "path";
const CACHE_DIR = join(import.meta.dirname, "..", ".cache");
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
function ensureCacheDir(): void {
if (!existsSync(CACHE_DIR)) {
mkdirSync(CACHE_DIR, { recursive: true });
}
}
function hashKey(key: string): string {
return createHash("sha256").update(key).digest("hex");
}
function getCachePath(key: string): string {
return join(CACHE_DIR, `${hashKey(key)}.json`);
}
export function getCached<T>(key: string): T | null {
const path = getCachePath(key);
if (!existsSync(path)) {
return null;
}
try {
const content = readFileSync(path, "utf-8");
const entry: CacheEntry<T> = JSON.parse(content);
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
// Cache expired
return null;
}
return entry.data;
} catch {
return null;
}
}
export function setCache<T>(key: string, data: T, ttl: number = DEFAULT_TTL_MS): void {
ensureCacheDir();
const entry: CacheEntry<T> = {
data,
timestamp: Date.now(),
ttl,
};
const path = getCachePath(key);
writeFileSync(path, JSON.stringify(entry, null, 2));
}
export async function fetchWithCache(url: string, ttl?: number): Promise<string> {
const cached = getCached<string>(url);
if (cached !== null) {
console.log(` [cache hit] ${url}`);
return cached;
}
console.log(` [fetching] ${url}`);
let lastError: Error | null = null;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const text = await response.text();
setCache(url, text, ttl);
return text;
} catch (error) {
lastError = error as Error;
if (attempt < 2) {
const delay = Math.pow(2, attempt) * 1000;
console.log(` [retry ${attempt + 1}] waiting ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}

View file

@ -1,186 +0,0 @@
import { createGenerator, type Config } from "ts-json-schema-generator";
import { existsSync, readFileSync } from "fs";
import { join, dirname } 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/@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, "..");
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, "..", "tsconfig.json"),
type: "*",
skipTypeCheck: true,
topRef: false,
expose: "export",
jsDoc: "extended",
};
try {
const generator = createGenerator(config);
const schema = generator.createSchema(config.type);
const definitions: Record<string, JSONSchema7> = {};
if (schema.definitions) {
for (const [name, def] of Object.entries(schema.definitions)) {
definitions[name] = def 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)`);
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
} catch (error) {
console.log(` [error] Schema generation failed: ${error}`);
console.log(" [fallback] Using embedded schema definitions");
return createFallbackSchema();
}
}
function createFallbackSchema(): NormalizedSchema {
// Fallback schema based on known SDK structure
const definitions: Record<string, JSONSchema7> = {
SDKMessage: {
type: "object",
properties: {
type: { type: "string", enum: ["user", "assistant", "result"] },
content: { type: "string" },
timestamp: { type: "string", format: "date-time" },
},
required: ["type"],
},
SDKResultMessage: {
type: "object",
properties: {
type: { type: "string", const: "result" },
result: { type: "object" },
error: { type: "string" },
duration_ms: { type: "number" },
},
required: ["type"],
},
Options: {
type: "object",
properties: {
model: { type: "string" },
maxTokens: { type: "number" },
temperature: { type: "number" },
systemPrompt: { type: "string" },
tools: { type: "array", items: { type: "string" } },
allowedTools: { type: "array", items: { type: "string" } },
workingDirectory: { type: "string" },
},
},
BashInput: {
type: "object",
properties: {
command: { type: "string" },
timeout: { type: "number" },
workingDirectory: { type: "string" },
},
required: ["command"],
},
FileEditInput: {
type: "object",
properties: {
path: { type: "string" },
oldText: { type: "string" },
newText: { type: "string" },
},
required: ["path", "oldText", "newText"],
},
FileReadInput: {
type: "object",
properties: {
path: { type: "string" },
startLine: { type: "number" },
endLine: { type: "number" },
},
required: ["path"],
},
FileWriteInput: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" },
},
required: ["path", "content"],
},
GlobInput: {
type: "object",
properties: {
pattern: { type: "string" },
path: { type: "string" },
},
required: ["pattern"],
},
GrepInput: {
type: "object",
properties: {
pattern: { type: "string" },
path: { type: "string" },
include: { type: "string" },
},
required: ["pattern"],
},
};
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
return createNormalizedSchema("claude", "Claude Code SDK Schema", definitions);
}

View file

@ -1,180 +0,0 @@
import { createGenerator, type Config } from "ts-json-schema-generator";
import { existsSync } 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, "..");
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...");
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, "..", "tsconfig.json"),
type: "*",
skipTypeCheck: true,
topRef: false,
expose: "export",
jsDoc: "extended",
};
try {
const generator = createGenerator(config);
const schema = generator.createSchema(config.type);
const definitions: Record<string, JSONSchema7> = {};
if (schema.definitions) {
for (const [name, def] of Object.entries(schema.definitions)) {
definitions[name] = def 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)`);
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
} catch (error) {
console.log(` [error] Schema generation failed: ${error}`);
console.log(" [fallback] Using embedded schema definitions");
return createFallbackSchema();
}
}
function createFallbackSchema(): NormalizedSchema {
// Fallback schema based on known SDK structure
const definitions: Record<string, JSONSchema7> = {
ThreadEvent: {
type: "object",
properties: {
type: {
type: "string",
enum: ["thread.created", "thread.updated", "item.created", "item.updated", "error"],
},
thread_id: { type: "string" },
item: { $ref: "#/definitions/ThreadItem" },
error: { type: "object" },
},
required: ["type"],
},
ThreadItem: {
type: "object",
properties: {
id: { type: "string" },
type: { type: "string", enum: ["message", "function_call", "function_result"] },
role: { type: "string", enum: ["user", "assistant", "system"] },
content: {
oneOf: [{ type: "string" }, { type: "array", items: { type: "object" } }],
},
status: { type: "string", enum: ["pending", "in_progress", "completed", "failed"] },
},
required: ["id", "type"],
},
CodexOptions: {
type: "object",
properties: {
apiKey: { type: "string" },
model: { type: "string" },
baseURL: { type: "string" },
maxTokens: { type: "number" },
temperature: { type: "number" },
},
},
ThreadOptions: {
type: "object",
properties: {
instructions: { type: "string" },
tools: { type: "array", items: { type: "object" } },
model: { type: "string" },
workingDirectory: { type: "string" },
},
},
Input: {
type: "object",
properties: {
type: { type: "string", enum: ["text", "file", "image"] },
content: { type: "string" },
path: { type: "string" },
mimeType: { type: "string" },
},
required: ["type"],
},
ResponseItem: {
type: "object",
properties: {
type: { type: "string" },
id: { type: "string" },
content: { type: "string" },
function_call: { $ref: "#/definitions/FunctionCall" },
},
},
FunctionCall: {
type: "object",
properties: {
name: { type: "string" },
arguments: { type: "string" },
call_id: { type: "string" },
},
required: ["name", "arguments"],
},
Message: {
type: "object",
properties: {
role: { type: "string", enum: ["user", "assistant", "system"] },
content: { type: "string" },
},
required: ["role", "content"],
},
};
console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`);
return createNormalizedSchema("codex", "Codex SDK Schema", definitions);
}

View file

@ -1,109 +0,0 @@
import { writeFileSync, existsSync, mkdirSync } from "fs";
import { join } from "path";
import { extractOpenCodeSchema } from "./opencode.js";
import { extractClaudeSchema } from "./claude.js";
import { extractCodexSchema } from "./codex.js";
import { extractAmpSchema } from "./amp.js";
import { validateSchema, type NormalizedSchema } from "./normalize.js";
const DIST_DIR = join(import.meta.dirname, "..", "dist");
type AgentName = "opencode" | "claude" | "codex" | "amp";
const EXTRACTORS: Record<AgentName, () => Promise<NormalizedSchema>> = {
opencode: extractOpenCodeSchema,
claude: extractClaudeSchema,
codex: extractCodexSchema,
amp: extractAmpSchema,
};
function parseArgs(): { agents: AgentName[] } {
const args = process.argv.slice(2);
const agentArg = args.find((arg) => arg.startsWith("--agent="));
if (agentArg) {
const agent = agentArg.split("=")[1] as AgentName;
if (!EXTRACTORS[agent]) {
console.error(`Unknown agent: ${agent}`);
console.error(`Valid agents: ${Object.keys(EXTRACTORS).join(", ")}`);
process.exit(1);
}
return { agents: [agent] };
}
return { agents: Object.keys(EXTRACTORS) as AgentName[] };
}
function ensureDistDir(): void {
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true });
}
}
async function extractAndWrite(agent: AgentName): Promise<boolean> {
try {
const extractor = EXTRACTORS[agent];
const schema = await extractor();
// Validate schema
const validation = validateSchema(schema);
if (!validation.valid) {
console.error(` [error] Schema validation failed for ${agent}:`);
validation.errors.forEach((err) => console.error(` - ${err}`));
return false;
}
// Write to file
const outputPath = join(DIST_DIR, `${agent}.json`);
writeFileSync(outputPath, JSON.stringify(schema, null, 2));
console.log(` [wrote] ${outputPath}`);
return true;
} catch (error) {
console.error(` [error] Failed to extract ${agent}: ${error}`);
return false;
}
}
async function main(): Promise<void> {
console.log("Agent Schema Extractor");
console.log("======================\n");
const { agents } = parseArgs();
ensureDistDir();
console.log(`Extracting schemas for: ${agents.join(", ")}\n`);
const results: Record<string, boolean> = {};
for (const agent of agents) {
results[agent] = await extractAndWrite(agent);
console.log();
}
// Summary
console.log("Summary");
console.log("-------");
const successful = Object.entries(results)
.filter(([, success]) => success)
.map(([name]) => name);
const failed = Object.entries(results)
.filter(([, success]) => !success)
.map(([name]) => name);
if (successful.length > 0) {
console.log(`Successful: ${successful.join(", ")}`);
}
if (failed.length > 0) {
console.log(`Failed: ${failed.join(", ")}`);
process.exit(1);
}
console.log("\nDone!");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View file

@ -1,129 +0,0 @@
import type { JSONSchema7 } from "json-schema";
export interface NormalizedSchema {
$schema: string;
$id: string;
title: string;
definitions: Record<string, JSONSchema7>;
}
/**
* Converts OpenAPI 3.1 schema to JSON Schema draft-07.
* OpenAPI 3.1 is largely compatible with JSON Schema draft 2020-12,
* but we want draft-07 for broader tool compatibility.
*/
export function openApiToJsonSchema(schema: Record<string, unknown>): JSONSchema7 {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(schema)) {
// Skip OpenAPI-specific fields
if (key === "discriminator" || key === "xml" || key === "externalDocs") {
continue;
}
// Handle nullable (OpenAPI 3.0 style)
if (key === "nullable" && value === true) {
continue; // Will be handled by type conversion
}
// Recursively convert nested schemas
if (key === "properties" && typeof value === "object" && value !== null) {
result[key] = {};
for (const [propName, propSchema] of Object.entries(value as Record<string, unknown>)) {
(result[key] as Record<string, unknown>)[propName] = openApiToJsonSchema(
propSchema as Record<string, unknown>
);
}
continue;
}
if (key === "items" && typeof value === "object" && value !== null) {
result[key] = openApiToJsonSchema(value as Record<string, unknown>);
continue;
}
if (key === "additionalProperties" && typeof value === "object" && value !== null) {
result[key] = openApiToJsonSchema(value as Record<string, unknown>);
continue;
}
if ((key === "oneOf" || key === "anyOf" || key === "allOf") && Array.isArray(value)) {
result[key] = value.map((item) =>
typeof item === "object" && item !== null
? openApiToJsonSchema(item as Record<string, unknown>)
: item
);
continue;
}
// Convert $ref paths from OpenAPI to local definitions
if (key === "$ref" && typeof value === "string") {
result[key] = value.replace("#/components/schemas/", "#/definitions/");
continue;
}
result[key] = value;
}
// Handle nullable by adding null to type array
if (schema["nullable"] === true && result["type"]) {
const currentType = result["type"];
if (Array.isArray(currentType)) {
if (!currentType.includes("null")) {
result["type"] = [...currentType, "null"];
}
} else {
result["type"] = [currentType as string, "null"];
}
}
return result as JSONSchema7;
}
/**
* Creates a normalized schema with consistent metadata.
*/
export function createNormalizedSchema(
id: string,
title: string,
definitions: Record<string, JSONSchema7>
): NormalizedSchema {
return {
$schema: "http://json-schema.org/draft-07/schema#",
$id: `https://sandbox-agent/schemas/${id}.json`,
title,
definitions,
};
}
/**
* Validates a schema against JSON Schema draft-07 meta-schema.
* Basic validation - checks required fields and structure.
*/
export function validateSchema(schema: unknown): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (typeof schema !== "object" || schema === null) {
return { valid: false, errors: ["Schema must be an object"] };
}
const s = schema as Record<string, unknown>;
if (s.$schema && typeof s.$schema !== "string") {
errors.push("$schema must be a string");
}
if (s.definitions && typeof s.definitions !== "object") {
errors.push("definitions must be an object");
}
if (s.definitions && typeof s.definitions === "object") {
for (const [name, def] of Object.entries(s.definitions as Record<string, unknown>)) {
if (typeof def !== "object" || def === null) {
errors.push(`Definition "${name}" must be an object`);
}
}
}
return { valid: errors.length === 0, errors };
}

View file

@ -1,55 +0,0 @@
import { fetchWithCache } from "./cache.js";
import { createNormalizedSchema, openApiToJsonSchema, type NormalizedSchema } from "./normalize.js";
import type { JSONSchema7 } from "json-schema";
const OPENAPI_URL =
"https://raw.githubusercontent.com/sst/opencode/dev/packages/sdk/openapi.json";
// Key schemas we want to extract
const TARGET_SCHEMAS = [
"Session",
"Message",
"Part",
"Event",
"PermissionRequest",
"QuestionRequest",
"TextPart",
"ToolCallPart",
"ToolResultPart",
"ErrorPart",
];
interface OpenAPISpec {
components?: {
schemas?: Record<string, unknown>;
};
}
export async function extractOpenCodeSchema(): Promise<NormalizedSchema> {
console.log("Extracting OpenCode schema from OpenAPI spec...");
const specText = await fetchWithCache(OPENAPI_URL);
const spec: OpenAPISpec = JSON.parse(specText);
if (!spec.components?.schemas) {
throw new Error("OpenAPI spec missing components.schemas");
}
const definitions: Record<string, JSONSchema7> = {};
// Extract all schemas, not just target ones, to preserve references
for (const [name, schema] of Object.entries(spec.components.schemas)) {
definitions[name] = openApiToJsonSchema(schema as Record<string, unknown>);
}
// Verify target schemas exist
const missing = TARGET_SCHEMAS.filter((name) => !definitions[name]);
if (missing.length > 0) {
console.warn(` [warn] Missing expected schemas: ${missing.join(", ")}`);
}
const found = TARGET_SCHEMAS.filter((name) => definitions[name]);
console.log(` [ok] Extracted ${Object.keys(definitions).length} schemas (${found.length} target schemas)`);
return createNormalizedSchema("opencode", "OpenCode SDK Schema", definitions);
}