mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 05:02:11 +00:00
add agent schemas
This commit is contained in:
commit
c4153c5335
20 changed files with 2735 additions and 0 deletions
3
resources/agent-schemas/.gitignore
vendored
Normal file
3
resources/agent-schemas/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
.cache/
|
||||
pnpm-lock.yaml
|
||||
3
resources/agent-schemas/Cargo.toml
Normal file
3
resources/agent-schemas/Cargo.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["engine/packages/*"]
|
||||
23
resources/agent-schemas/package.json
Normal file
23
resources/agent-schemas/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "agent-schemas",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"ts-json-schema-generator": "^2.4.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"@anthropic-ai/claude-code": "latest",
|
||||
"@openai/codex": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
271
resources/agent-schemas/src/amp.ts
Normal file
271
resources/agent-schemas/src/amp.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
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);
|
||||
}
|
||||
94
resources/agent-schemas/src/cache.ts
Normal file
94
resources/agent-schemas/src/cache.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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;
|
||||
}
|
||||
186
resources/agent-schemas/src/claude.ts
Normal file
186
resources/agent-schemas/src/claude.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
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);
|
||||
}
|
||||
180
resources/agent-schemas/src/codex.ts
Normal file
180
resources/agent-schemas/src/codex.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
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);
|
||||
}
|
||||
109
resources/agent-schemas/src/index.ts
Normal file
109
resources/agent-schemas/src/index.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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);
|
||||
});
|
||||
129
resources/agent-schemas/src/normalize.ts
Normal file
129
resources/agent-schemas/src/normalize.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
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-daemon/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 };
|
||||
}
|
||||
55
resources/agent-schemas/src/opencode.ts
Normal file
55
resources/agent-schemas/src/opencode.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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);
|
||||
}
|
||||
16
resources/agent-schemas/tsconfig.json
Normal file
16
resources/agent-schemas/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue